系列:一張投影片背後的工程:從零打造網頁簡報編輯器
對應程式:2026-10-04 工作目錄,
package.json版本0.20.0。本文數據來自當日 Edge 實測,原始資料另存 JSON;不以測試執行時間推算操作延遲。
前幾篇讓編排功能變得更完整,今天換一個問題:文件變大以後,一段文字修改會留下多少歷史?Day 21 曾補充連續輸入合併,這次我用固定資料比較它實際減少了什麼。
結果先說:在這次條件下,合併明確減少了保留的歷史與物件版本;純 reducer 的耗時差距很小,還不能宣稱使用者輸入因此變快。
我用取證腳本在獨立 Edge 設定檔載入目前專案的實際 reducer,建立三組資料:
| 頁數 | 每頁內容 | 總物件數 |
|---|---|---|
| 10 | 15 個文字、4 個矩形、1 個圖片引用 | 200 |
| 50 | 相同組成 | 1,000 |
| 100 | 相同組成 | 2,000 |
每組都對第一頁的同一個文字框提交 500 次不同內容,文字長度保持一致。這是固定負載,沒有模擬真實逐字增長,也沒有產生 500 次鍵盤事件。
對照組不傳 groupKey,每次更新各留一步,最後受 50 筆歷史上限限制;合併組全部傳相同的 groupKey。兩組都使用目前版本的同一個 reducer,不是切換歷史版本比較。
測量環境為 Windows、AMD Ryzen 7 9800X3D、Edge 154.0.4258.53,使用 Vite 開發伺服器、無介面 Edge 並停用 GPU。每種模式先暖機三次,再交錯順序取九次計時樣本;表中是中位數。
500 次更新後,對照組保留 50 筆 past,合併組保留 1 筆。兩組最後的文字都必須是「測量文字 0499」,量測時也檢查這個結果,避免把漏做更新當成變快。
以十頁為例,從 past 加上 present 出發,用 Set 依物件身分計算:
| 項目 | 每次獨立記錄 | 同一欄位合併 |
|---|---|---|
| 過去文件版本 | 50 | 1 |
| 不同投影片物件 | 60 | 11 |
| 不同文字/圖片/圖形物件 | 250 | 201 |
十頁原本共有 200 個物件,最後沒有變成 51 × 200 個副本。沒有修改的投影片與物件仍共用參照;更新集中在第一頁的那個文字框。
這比把每份歷史 JSON 的長度全部相加更接近實際保留關係。JSON 序列化會展開重複內容,不能直接把它的總長度當作 JavaScript heap 使用量。
| 頁數 | 獨立記錄:500 次更新 | 合併:500 次更新 | 獨立記錄:保留 heap 增量 | 合併:保留 heap 增量 |
|---|---|---|---|---|
| 10 | 0.3 ms | 0.2 ms | 13,732 bytes | 336 bytes |
| 50 | 0.8 ms | 0.7 ms | 32,280 bytes | 10,484 bytes |
| 100 | 1.4 ms | 1.4 ms | 37,492 bytes | 6,096 bytes |
時間是整批 500 次同步 reducer 呼叫,沒有包含 React 更新畫面、IndexedDB、自動儲存或圖片解碼。數值接近計時解析度,不能把相差 0.1 毫秒解讀成穩定的使用者體感改善。
記憶體則在強制垃圾回收後,讀取 Runtime.getHeapUsage 的 usedSize,比較保留最後一份 history 前後的差值,重做五次取中位數。基礎文件與預先建立的 actions 已存在於兩邊的基準中。這個 API 回報 JavaScript heap 資訊,不是整個瀏覽器行程的記憶體。Chrome DevTools Protocol:Runtime.getHeapUsage
像五十頁合併組的增量高於一百頁,就提醒我:這裡還包含 V8 配置與回收的測量雜訊,不能拿幾個數字推導線性公式。比較可靠的結論,是本次各組都減少了保留量,而 50 步降為 1 步的歷史行為是可直接驗證的。
實作中決定是否延續歷史的部分如下:
const continuesGroup = action.groupKey !== undefined &&
action.groupKey === history.groupKey
past: continuesGroup
? history.past
: [...history.past, history.present].slice(-HISTORY_LIMIT)
即使沿用 past,每次仍會呼叫 presentationReducer() 建立新的 present,並保留不可變更新的行為。因此「只留一步」不等於「只執行一次更新」。
原本連續輸入容易快速耗盡 50 步,讓先前其他操作擠出歷史;合併既降低保留成本,也讓一次復原能退回整段編輯。這次驗證的是已完成的改進效果,沒有為了讓數字好看而把必要更新跳過。
除了純函式量測,腳本也把十頁、每頁二十個物件存進 IndexedDB,再重新載入編輯器。十張縮圖、十張列印頁,以及十個圖片引用都存在,PNG 成功解碼。
這十頁共用一筆合成 PNG 資產。它驗證此資料集的載入與呈現,不代表完成 20 MB 圖片、多張不同大圖或一百頁畫面流暢度驗收。
本次沒有量測拖曳幀率、輸入到畫面更新的延遲、儲存等待或整個瀏覽器的峰值記憶體。頁數增加時 reducer 批次耗時上升,值得後續追查;目前證據還不足以認定它就是整個編輯器的主要瓶頸。
上面的資料組成、更新次數、兩組唯一的操作差異、歷史筆數、物件身分數與中位數,都寫在本文中。讀者可以據此判斷結論是否超過測量範圍:這次支持「歷史保留量減少」,沒有證明整個編輯器的畫面更新變快。
完整九次計時與五次記憶體樣本目前只保存在我的本機驗證資料中。文章尚未提供公開的程式或原始數據連結,所以這些數字現階段不能由讀者下載腳本逐項重跑。下一篇會用可見的 Demo 畫面整理作品成果,並清楚區分文章能展示的內容與尚未公開的檔案。