iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
Modern Web

現代函式庫與JavaScript的關係系列 第 21

Day 21 | 先假裝成功 — 樂觀更新買到的是 100 毫秒,不是「快十倍」

  • 分享至 

  • xImage
  •  

三個提問

  1. 昨天說樂觀更新能讓介面「感覺快十倍」,到底快在哪裡?我按下按鈕之後省掉的是什麼?
  2. 「失敗再回滾」聽起來很簡單,為什麼大家都說它最容易寫錯?
  3. React 19 出了 useOptimistic,它跟 TanStack Query 的 onMutate 是同一件事的兩種寫法嗎?

先更正我自己昨天講的一句話。

Day 20 的明天預告我寫了「能讓介面感覺快十倍」。實際量完以後,「快十倍」是錯的說法,它既低估也高估了這件事。今天第二節會用數字講清楚為什麼。


一、承接 Day 20:從「等回應」到「不等回應」

Day 20 講的是讀取useQuery 怎麼靠 queryKey 避開競態,怎麼把結果存進自己的格子。

今天講寫入。寫入跟讀取有一個根本差異:

讀取的時候,畫面上本來就沒有東西,使用者對「等一下」有心理準備。
寫入的時候,畫面上已經有東西了,使用者的動作是「改它」,他期待它立刻變。

所以同樣是 600 毫秒的網路來回,發生在讀取時叫「載入中」,發生在寫入時叫「這個網站好卡」。

兩種寫法

先把名詞定下來,後面整篇都用這兩個詞。

名稱 做法 畫面什麼時候變
悲觀更新(pessimistic update) 送出請求,等伺服器回應,用回應改畫面 伺服器回來之後
樂觀更新(optimistic update) 先改畫面,同時送出請求,回來之後用真實資料校正 使用者按下去的那一刻

「樂觀」這個詞的意思是:你賭它會成功,所以提前把成功的畫面畫上去。

賭輸了怎麼辦,就是第三節之後的全部內容。


二、量測:到底買到了什麼

情境是一份活動手冊,使用者按「發布」把 statusdraft 改成 published。假伺服器固定 600 毫秒回應。

我在 day21-optimistic-update.js 裡量「使用者按下按鈕之後,畫面第一次真的改變是在第幾毫秒」。

悲觀更新

使用者在第 0ms 按下「發布」
  [伺服器    1ms] 收到請求 #1 發布 {"status":"published"}
  [伺服器  602ms] 請求 #1 發布 成功,db 變成 status=published
  [畫面    603ms] status=published

樂觀更新

使用者在第 0ms 按下「發布」
  [畫面      0ms] status=published        ← 樂觀值,伺服器還沒回
  [伺服器    0ms] 收到請求 #1 發布 {"status":"published"}
  [伺服器  601ms] 請求 #1 發布 成功,db 變成 status=published
  [畫面    601ms] status=published        ← 伺服器回來了,用真實資料校正

量測結果

悲觀更新:按下按鈕後 603ms 畫面才有反應
樂觀更新:按下按鈕後   0ms 畫面就有反應
差距:603ms
倍數:無法用倍數表達(分母是 0)

這就是為什麼「快十倍」是錯的說法。

分母是零,倍數這個詞在這裡根本沒有意義。如果伺服器要跑 3 秒,樂觀更新也一樣是 0 毫秒,難道就變成「快三十倍」嗎?倍數會隨著伺服器有多慢而浮動,但使用者的感受不是這樣運作的。

正確的講法:它跨過了一條線

Jakob Nielsen 在 1993 年的 Usability Engineering 裡整理了三個反應時間界線,這三個數字到今天還在用:

界線 使用者的感受
0.1 秒以內 覺得是「系統即時反應」,不需要任何載入提示
1 秒以內 思緒不中斷,但已經感覺到在等
10 秒以內 注意力還在這個任務上,超過就會跑去做別的事

把量測結果放進去看:

  • 悲觀更新的 603 毫秒,落在第二區間,使用者感覺得到在等,所以你必須放一個 spinner 安撫他
  • 樂觀更新的 0 毫秒,落在第一區間,使用者覺得是即時的,連 spinner 都不用放

樂觀更新做的事情是「跨過 100 毫秒那條線」,不是「把時間除以十」。

這個區別不只是講法精確而已,它會改變你的判斷:如果你的 API 本來就只要 60 毫秒,那你已經在第一區間了,樂觀更新對使用者沒有任何差別,你只是多寫了回滾的程式碼。判斷標準不是「能不能更快」,是「現在在哪一區間」。


三、沒有備份的樂觀更新,會留下一個騙人的畫面

樂觀更新最直覺的寫法長這樣:

cache = { ...cache, status: 'published' }   // 先改畫面
screen.render(cache)

try {
  const result = await server.patch({ status: 'published' })
  cache = result
} catch (error) {
  // 想回滾,但舊值早就被上面那行蓋掉了
}

白話講這段:cache = { ...cache, status: 'published' } 展開舊的 cache 再覆蓋 status產生一個新物件指派回 cache。這一行執行完,原本那個舊物件就沒有任何變數指著它了,你想回滾也找不回來。

這是 Day 3「傳值與傳址」那篇的直接應用:{ ...cache } 是淺拷貝,它產生新的 reference,而 cache 這個變數被重新指向新的那一份。舊的那份不是被改掉,是被放生了。

實測結果:

【沒備份】
  [畫面    0ms] status=published        ← 樂觀值
  [伺服器  601ms] 請求 #1 發布 失敗(500)
  [程式    602ms] 想回滾,但手上沒有舊值,只能什麼都不做

  畫面最後顯示 status=published,資料庫是 status=draft
【有備份】
  const previous = cache                  ← 只差這一行
  [畫面    0ms] status=published        ← 樂觀值
  [伺服器  601ms] 請求 #1 發布 失敗(500)
  [畫面    602ms] status=draft           ← 回滾

  畫面最後顯示 status=draft,資料庫是 status=draft

沒備份的那個版本,最後留下的畫面是:

使用者以為自己發布成功了,其實資料庫從頭到尾都是草稿。

而且它不會自己修好。要等到使用者重新整理、切分頁回來觸發 refetch、或是下一次 invalidateQueries,畫面才會變回真相。在那之前,你的前端在對使用者說謊。

這就是為什麼 TanStack Query 的 onMutate 一定要 return { previousTodos }

onMutate: async (newTodo, context) => {
  await context.client.cancelQueries({ queryKey: ['todos'] })
  const previousTodos = context.client.getQueryData(['todos'])
  context.client.setQueryData(['todos'], (old) => [...old, newTodo])
  return { previousTodos }                    // ← 這一行就是上面的 const previous = cache
},
onError: (err, newTodo, onMutateResult, context) => {
  context.client.setQueryData(['todos'], onMutateResult.previousTodos)
},
onSettled: () => context.client.invalidateQueries({ queryKey: ['todos'] })

白話講這四行的拋接關係:

  • cancelQueries 先把正在飛的 refetch 取消掉,否則那個舊的 GET 回來會把你剛寫進去的樂觀值蓋掉
  • getQueryData 把此刻快取裡的值讀出來存成 previousTodos
  • setQueryData 把樂觀值寫進快取,畫面立刻變
  • onMutate 回傳的物件會被 TanStack Query 收好,等一下原封不動交給 onError,這就是備份的傳遞路徑

這裡有一個很多人誤解的點要先講清楚:

cancelQueries 擋的是「查詢」跟「修改」打架,不是「修改」跟「修改」打架。

它取消的是 in-flight 的 useQuery refetch。兩個 useMutation 同時在飛的時候,cancelQueries 幫不上忙——那正是下一節的主題。


四、回滾最容易出錯的那一步

到目前為止都還很單純,因為畫面上只有一個操作在飛。

現實不是這樣。使用者按了「發布」,等了 30 毫秒沒反應,順手又改了標題。現在有兩個 mutation 同時在空中。

劇本

操作 伺服器耗時 結果
A 按「發布」(改 status 600 ms 失敗(500)
B 改標題(改 title 150 ms 成功

兩邊都照教科書寫:onMutate 備份整份快取,onError 寫回整份快取。這是正確的單一 mutation 寫法。

實測時間軸

  [A    0ms] onMutate 備份 status=draft     title=場地規範
  [畫面   0ms] status=published title=場地規範        ← A 的樂觀值
  [B   31ms] onMutate 備份 status=published title=場地規範
  [畫面  31ms] status=published title=場地規範 v2     ← B 的樂觀值
  [伺服器 182ms] 請求 #2 B 改標題 成功,db 變成 status=draft title=場地規範 v2
  [畫面 182ms] status=draft     title=場地規範 v2     ← B 成功,用伺服器回應校正
  [伺服器 601ms] 請求 #1 A 發布 失敗(500)
  [畫面 601ms] status=draft     title=場地規範        ← A 失敗,寫回 A 的快照

  畫面最後顯示:status=draft title=場地規範
  資料庫真相:  status=draft title=場地規範 v2
  一致嗎:不一致

發生了什麼

a. A 的 onMutate 備份了「發布之前」的整份資料 → { draft, 場地規範 }

b. B 的 onMutate 備份了「B 開始時」的整份資料 → 裡面含有 A 的樂觀值 { published, 場地規範 }

c. B 成功,用伺服器回應蓋掉整份快取 → A 的樂觀值 published 被抹掉,畫面在第 182 毫秒閃回草稿

d. A 失敗,把 A 的快照寫回去 → 連 B 已經寫進資料庫的新標題一起抹掉

使用者看到的是:標題改成 v2 了,然後自己變回去了。而資料庫裡明明存的是 v2。

一句話講清楚問題出在哪

回滾寫回的是「那個 mutation 開始時的整個世界」,不是「那個 mutation 改的那一個欄位」。

單一操作看不出差別,因為「整個世界」剛好就等於「你那個欄位」。
 一旦併發,快照就會把別人的成功結果一起裝進去,然後在回滾時一起還原掉。

這也解釋了為什麼這個 bug 在本機幾乎不會出現——跟 Day 20 的競態條件同一個理由:本機的延遲太小,兩個 mutation 撞在一起的機率太低。


五、兩種修法

修法一:欄位級回滾

回滾的時候只還原自己動過的那幾個 key,其他欄位維持現狀。

function 樂觀補丁(patch) {
  const backup = {}
  for (const key of Object.keys(patch)) backup[key] = cache[key]
  cache = { ...cache, ...patch }
  return {
    rollback() {
      cache = { ...cache, ...backup }      // 只還原自己那幾個 key
    },
  }
}

白話講這段:Object.keys(patch) 拿出這次要改的欄位名單,只備份名單上的那幾個,不備份整份。回滾時也用展開語法把備份疊回現在的 cache 上,所以別人在這段期間改的欄位完全不受影響。

實測:

  [畫面 181ms] status=published title=場地規範 v2   ← B 成功,不覆蓋其他欄位
  [畫面 601ms] status=draft     title=場地規範 v2   ← A 失敗,只還原 status

  畫面:  status=draft title=場地規範 v2
  資料庫:status=draft title=場地規範 v2
  一致嗎:一致

代價:成功的時候不能直接用整份伺服器回應蓋快取。伺服器自己算出來的欄位(updatedAtversion、後端算的 slug)你就拿不到了。

修法二:讓伺服器當唯一真相

回滾只是過渡畫面,最後一律重新去問伺服器。對應的就是 onSettled 裡那行 invalidateQueries

但有一個細節要處理:如果每個 mutation 落地都 refetch 一次,兩個 mutation 就會打兩次 GET,而且先回來的那次可能是舊資料。所以要數還有幾個在飛

let 飛在空中的數量 = 0

async function 收尾() {
  飛在空中的數量 -= 1
  if (飛在空中的數量 > 0) return        // 還有人在飛,先不 refetch
  const fresh = await server.get()
  cache = fresh
}

實測:

  [畫面 182ms] status=draft title=場地規範 v2      ← B 成功
  [收尾 182ms] 還有 1 個 mutation 在飛,先不 refetch
  [畫面 601ms] status=draft title=場地規範         ← A 失敗,回滾(只是過渡畫面)
  [收尾 601ms] 全部落地,向伺服器重新拿一次
  [畫面 701ms] status=draft title=場地規範 v2      ← refetch 的結果,這才是最終答案

  一致嗎:一致

代價:多一次 GET,而且要等它回來。注意第 601 毫秒到第 701 毫秒之間,畫面會先閃一下錯的回滾值,才被 refetch 校正。使用者看得到那一閃。

(TanStack Query 內建的 mutation scope 與 isMutating 可以達到同樣的效果,這裡手寫是為了讓「為什麼要數」這件事看得見。)

兩種修法的取捨

修法一:欄位級回滾 修法二:伺服器收尾
最終一致 一致 一致
額外請求 0 每批 mutation 多一次 GET
中間會不會閃錯的值 不會 會閃一下
拿不拿得到伺服器算的欄位 拿不到 拿得到
實作複雜度 要自己管 patch 的 key 要自己數在飛的數量

沒有哪一個是標準答案。判準是:你的資料裡有沒有「伺服器才算得出來的欄位」。 有的話只能走修法二,沒有的話修法一更乾淨。


六、第三種模型:不備份,改成疊一層

前面兩種修法都在處理同一個結構問題:樂觀值被寫進了真相裡,所以要想辦法把它挖出來。

那如果從一開始就不要寫進去呢?

function createOptimisticStore(initial) {
  let base = { ...initial }            // 伺服器真相
  const overlays = new Map()           // 還在飛的樂觀值
  let nextId = 1

  const view = () => {
    let result = { ...base }
    for (const patch of overlays.values()) result = { ...result, ...patch }
    return result
  }

  return {
    get value() { return view() },
    setBase(next) { base = { ...next } },
    addOverlay(patch) {
      const id = nextId++
      overlays.set(id, patch)
      return () => overlays.delete(id)   // 回傳「拿掉這一層」的函式
    },
  }
}

白話講這段:base 是伺服器真相,overlays 是一疊還沒落地的樂觀值。畫面讀的 view()每次即時把所有層疊起來算出來的,不是存在某個變數裡的。addOverlay 回傳的那個函式是這一層的橡皮擦,呼叫它就只擦掉自己這一層。

用同一個併發劇本跑:

  [畫面   0ms] status=published title=場地規範      ← 疊上 A 的樂觀層
  [畫面  30ms] status=published title=場地規範 v2   ← 疊上 B 的樂觀層
  [畫面 181ms] status=published title=場地規範 v2   ← 拿掉 B 的樂觀層(base 已更新)
  [畫面 601ms] status=draft     title=場地規範 v2   ← 拿掉 A 的樂觀層

  一致嗎:一致
  剩下幾層樂觀值:0

整段程式碼裡沒有出現 previous,沒有出現 rollback

因為回滾在這個模型裡不是一個動作,而是「那一層自然消失」。A 失敗的時候不需要知道世界原本長什麼樣,它只要把自己那一層拿掉,剩下的 base 加上 B 的層自己就是正確答案。

這就是 React 19 useOptimistic 的模型

const [optimisticState, addOptimistic] = useOptimistic(value, reducer)

React 官方文件對回傳值的描述是:

optimisticState:當前的樂觀狀態。在沒有 Action 進行中時,它等於 value;有 Action 進行中時,它等於 reducer 算出來的狀態。

以及這句,正好對應上面那個「自然消失」:

「沒有額外的一次 render 來『清除』樂觀狀態。樂觀狀態與真實狀態會在 Transition 結束的那一次 render 裡收斂。」

這一句解釋了 useOptimistic 比第五節的修法二好在哪裡:修法二那個「先閃一下回滾值、再被 refetch 校正」的中間畫面,在這個模型裡不存在,因為拿掉疊加層跟更新 base 發生在同一次 render。

失敗的情況官方文件也寫得很明確:

「如果 Action 拋出錯誤,Transition 一樣結束,React 就用 value 當下的值去 render。既然上層通常只在成功時更新 value,失敗就代表 value 沒有變過,所以畫面會顯示樂觀更新之前的樣子。」

換句話說,useOptimistic 沒有提供回滾 API,是因為它的模型裡不需要回滾這個動作。

回到你的第三個提問

React 19 的 useOptimistic 跟 TanStack Query 的 onMutate 是同一件事的兩種寫法嗎?

不是。它們是兩種不同的模型:

onMutatesetQueryData useOptimistic
樂觀值放哪裡 寫進快取本體 疊在真相上面的一層
回滾怎麼做 把備份寫回去(要 previous 拿掉那一層(不需要備份)
併發時會不會抹掉別人 ,除非做第五節的修法 結構上不會
多個元件看得到嗎 看得到(快取是共用的) 只在宣告它的那棵子樹
前提 你有一個快取層 你有一個明確的「真相」來當 base

值得注意的是,TanStack Query 官方文件現在把「Via the UI」列在「Via the Cache」前面,而 Via the UI 的做法正是讀 useMutationvariables 來畫暫時的 UI——那也是疊加層模型。官方的建議是:

「如果你只有一個地方需要顯示樂觀結果,用 variables 直接更新 UI 是程式碼比較少、也比較容易推理的做法。」

兩邊都在往疊加層走。 差別只在 React 把它做進了框架,TanStack Query 把它留在 hook 的回傳值裡。


七、橫向比較:其他框架在這個岔路口選了什麼

這個系列的慣例,每隔幾天要橫向看一次。樂觀更新這件事,各家的分歧不在 API,而在它被放在哪一層

框架/工具 放在哪一層 模型
React 19 框架本身(useOptimistic + Actions) 疊加層
TanStack Query(React/Vue/Svelte/Solid 都有) 資料層 兩種都提供,文件現在推疊加層
SvelteKit 表單層(use:enhance 提交中的 FormData 就是那一層
React Router/Remix 路由層(useFetcher().formData 同上,比 useOptimistic 更早採用

看出共同點了嗎:後來的設計都把樂觀值放在「提交這個動作本身」上,而不是放進狀態容器裡。

理由就是第四節那個 bug。只要樂觀值跟真相住在同一個盒子裡,你就永遠要處理「誰的快照蓋掉誰」。把它掛在提交上,提交結束它自己就沒了。

use:enhanceuseFetcher 的細節我沒有在這個系列實作過,這一段是讀文件後的整理,不是實測。)


八、什麼時候不該用

系列慣例,一定要有這一節。四種情況我不會做樂觀更新:

a. 你的 API 本來就在 100 毫秒以內

第二節那條線。已經在第一區間了,樂觀更新對使用者沒有任何差別,你只是多寫了回滾。先去量,不要先寫。

b. 失敗的代價很高

付款、下單、送出考卷、按「確認刪除」。這些操作先顯示成功再說抱歉,比讓使用者等 600 毫秒糟糕得多。 判準是:如果回滾發生在使用者已經關掉分頁之後,他會不會受到實質損失。

c. 伺服器回來的東西你猜不到

前端沒辦法自己算出正確的樂觀值的時候。例如送出訂單以後編號是後端發的、折扣是後端算的、排序位置由後端決定。你硬猜一個放上去,回來以後畫面會跳一下,那一跳比 loading 更難看。

d. 同一份資料有多個使用者同時在改

協作編輯、共用看板。這時候要的不是樂觀更新,是 CRDT 或 OT 那一套衝突解決機制。樂觀更新的前提是「只有我在改」,這個前提一破,回滾的語意就不成立了。


九、今天的判斷標準

看到一段「先改畫面再送請求」的程式碼時,問四個問題:

問題 如果答案是「否」
我的 API 有超過 100 毫秒嗎? 不需要樂觀更新,先去量
我有把舊值存起來嗎? 失敗時畫面會留在騙人的狀態,而且不會自己修好
我回滾的是「我改的那幾個欄位」嗎? 併發時會抹掉別人的成功結果
我的樂觀值跟真相是分開存的嗎? 你遲早要寫第五節那兩種修法其中之一

還有一個更根本的心態:

樂觀更新不是效能優化,是一個關於「畫面可以暫時不等於真相」的設計決定。

效能優化做錯了,是慢;這件事做錯了,是畫面在說謊。所以它的難點從來不在怎麼提前改畫面,而在怎麼讓謊言一定會被收回。


明天預告

Day 22 講 React 19 的 Actions 與 useActionState:今天的 useOptimistic 其實只是 Actions 這一整套裡的一塊,明天把 <form action={fn}>useActionStateuseFormStatus 跟 pending 狀態一起講完,順便回答「React 為什麼把表單交回給瀏覽器」。


完整可執行程式碼

存成 day21-optimistic-update.js,執行 node day21-optimistic-update.js 可以重跑本文全部五段實測。純 Node.js 不需要任何套件,也不需要 React——樂觀更新與回滾是非同步本身的問題,跟框架無關,這一點跟 Day 20 的競態條件一樣。

檔案分成五段,對應本文的第二到第六節:

Part 內容 對應章節
1 悲觀與樂觀的時間差,以及 Nielsen 的三條線 第二節
2 失敗回滾:有備份與沒備份 第三節
3 兩個 mutation 同時在飛,整份快照回滾出事 第四節
4 兩種修法:欄位級回滾、伺服器收尾 第五節
5 疊加層模型,20 行的樂觀更新引擎 第六節

核心的那 20 行(Part 5)直接貼在這裡,其餘請看檔案:

function createOptimisticStore(initial) {
  let base = { ...initial }                 // 伺服器真相
  const overlays = new Map()                // 還在飛的樂觀值,key 是 mutation id
  let nextId = 1

  const view = () => {
    let result = { ...base }
    for (const patch of overlays.values()) result = { ...result, ...patch }
    return result
  }

  return {
    get value() { return view() },
    setBase(next) { base = { ...next } },
    addOverlay(patch) {
      const id = nextId++
      overlays.set(id, patch)
      return () => overlays.delete(id)
    },
    get overlayCount() { return overlays.size },
  }
}

參考來源與內容出處說明

延續這個系列的做法,把內容分類標示。

一、有正式出處的部分

內容 出處
useOptimistic 的簽名、回傳值、以及「樂觀狀態只在 Action 進行中 render」 React 官方文件:https://react.dev/reference/react/useOptimistic
「沒有額外的一次 render 來清除樂觀狀態,兩者在 Transition 結束的同一次 render 收斂」 同上,Reference 段落
「Action 拋錯時 Transition 一樣結束,畫面回到 value 當下的值」 同上
onMutateonErroronSettled 的完整回滾範例,以及 cancelQueriesgetQueryDatasetQueryData 的角色 TanStack Query 官方文件:https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates
「只有一個地方要顯示樂觀結果時,用 variables 更新 UI 程式碼較少也較好推理」 同上,Via the UI 段落
0.1 秒/1 秒/10 秒三個反應時間界線 Jakob Nielsen, Usability Engineering(1993),Nielsen Norman Group 線上版:https://www.nngroup.com/articles/response-times-3-important-limits/
SvelteKit use:enhance 在提交期間可取用 FormData Svelte 官方教學:https://svelte.dev/tutorial/kit/customizing-use-enhance

二、我實際跑出來的部分

第二到第六節所有的毫秒數與時間軸,全部由 day21-optimistic-update.js 實測產生(Node.js v22.22.2,2026-09-22 執行),可以重跑驗證。包含:

  • 悲觀 603 毫秒 vs 樂觀 0 毫秒
  • 沒備份時畫面停在 published 而資料庫是 draft
  • 併發整份快照回滾後「畫面 場地規範/資料庫 場地規範 v2」的不一致
  • 兩種修法各自達成一致,以及修法二在 601~701 毫秒之間會閃一次錯值

Part 5 的 createOptimisticStore 是我寫的極簡模型,不是 React 的原始碼。 它只示範「樂觀值是疊加層而不是寫進真相」這一個設計概念,useOptimistic 真實的實作牽涉 Transition、排程與並行 render,複雜得多。

三、我自己的整理與判斷(沒有外部出處)

  • 「樂觀更新買到的是跨過 100 毫秒那條線,不是快十倍」這個講法,以及據此推出的「API 已經在 100 毫秒內就不需要樂觀更新」判準
  • 第四節那個併發劇本的設計(A 慢且失敗、B 快且成功、間隔 30 毫秒),是我為了讓問題明顯而刻意排的
  • 「回滾寫回的是整個世界,不是你那個欄位」這句總結
  • 第五節兩種修法的取捨表,以及「判準是你的資料裡有沒有伺服器才算得出來的欄位」
  • 第七節「後來的設計都把樂觀值掛在提交這個動作上」這個觀察
  • 第八節四種不該用的情況,以及「如果回滾發生在使用者關掉分頁之後,他會不會受到實質損失」這個判準
  • 「樂觀更新不是效能優化,是一個關於畫面可以暫時不等於真相的設計決定」這句收尾

四、我沒有實作驗證的部分

第七節表格裡的 SvelteKit use:enhance 與 React Router/Remix useFetcher().formData,我是讀文件整理的,沒有在這個系列裡實際寫過。如果你要照著用,請以各自的官方文件為準。

五、關於昨天那句「快十倍」

Day 20 的明天預告是先寫的,數字是今天才量的。量完發現倍數這個講法站不住腳(分母是 0),所以今天第二節直接更正。預告寫在實測之前是我流程上的疏忽,留著不刪是為了讓修正這件事本身留在系列裡。

(查閱日期:2026-09-22。程式碼實測於 Node.js v22.22.2)


上一篇
Day 20 | API 呼叫永遠在賽跑 — React Query/TanStack Query 怎麼管理遠端資料狀態
下一篇
Day 22 | 框架與 API 交互—React / Vue / Angular 呼叫 Gemini 的寫法差異與效能
系列文
現代函式庫與JavaScript的關係22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言