工程師專區 · Playground

給 reviewer 看的 PR 導讀

你開了一個改 8 個檔案的 PR、reviewer 打開看到一坨 diff 不知從何看起、丟一句「能不能簡單講一下動機?」你回 GitHub PR description 又懶得寫詳細、結果他猜錯重點、修錯地方、來回 5 輪。 這個 demo 把 PR description 做成「動機 → before/after → 一檔一檔導讀 → 我希望你重點看哪」的單頁、reviewer 點一個 file 看一段、5 分鐘抓到全貌。 叫 AI 把你 PR 的 git log 跟 diff 變成這份頁面、不是給你一段冷冰冰的 PR template。

動機

上週 prod 出現一次 incident:使用者上傳 5MB 以上的圖片、後端記憶體飆 1.2GB、撐了 20 秒才回 500。 事後追下來是 image-processor 用 sync API 把整張圖讀進 buffer 再 resize、單 request 就佔住 worker。

這個 PR 把 image-processor 改用 stream API、加 size guard、加 backpressure。 目標:單 request 記憶體上限 80MB、超過直接 413 reject、不再讓單一上傳拖垮 worker。

前後對比

// processor.ts (Before)
export async function processImage(req: Request) {
  const buf = await req.arrayBuffer();   // ← 整張讀進記憶體
  const img = sharp(Buffer.from(buf));   // ← 又一份 copy
  return img.resize(800).toBuffer();
}
→ 5MB 圖片 → ~15MB heap、10 個並發 = 150MB worker。
// processor.ts (After)
export async function processImage(req: Request) {
  if (Number(req.headers['content-length']) > 5_000_000) {
    throw new HttpError(413, 'image too large');
  }
  return pipeline(
    req,                                  // ← stream 進
    sharp().resize(800),                  // ← stream 過
    res                                   // ← stream 出
  );
}
→ 同樣 5MB 圖片 → ~80KB heap、10 個並發 = 800KB worker。

數字

  • 單 request 記憶體:15MB → 0.08MB(local benchmark、Node 20、5MB JPEG)
  • p95 處理時間:820ms → 410ms(同硬體、stream 不用等整張讀完)
  • 10 並發 worker RSS:180MB → 92MB

一檔一檔看

共改 6 個檔、依重要度排序。每個 file 點開看「為什麼改 + 改了什麼 + 注意什麼」。

src/image/processor.ts +24-18主檔

為什麼改:整個 PR 的核心。把 arrayBuffer() 拿掉、改用 pipeline(req, sharp(), res)

注意:

  • content-length 可被 client 偽造、但作為第一層 guard 還是省 90% 案例。第二層 guard 在 middleware/size-limit.ts
  • error 改用 HttpError 不是 throw 普通 Error、是為了讓 error-handler middleware 接得到
src/middleware/size-limit.ts +42-0新增

為什麼改:第二層 guard、直接從 stream 累計 bytes、超過 5MB 就斷 stream。比 content-length 可靠。

注意:middleware 順序很重要、必須掛在 multer / body parser 之前、不然 stream 已經被消費過。

src/errors/http-error.ts +18-0新增

為什麼改:新增一個帶 statusCode 的 Error class、給 processor 跟未來其他 handler 共用。

注意:已有 AppError、但它沒帶 statusCode、不想破壞既有 contract、所以新建。後續可以考慮統一。

src/middleware/error-handler.ts +8-2小改

為什麼改:讓 error handler 認得 HttpError、用它的 statusCode 回 client。

tests/image-processor.test.ts +86-12主要測試

為什麼改:加 4 個 test case:(1) 正常 1MB 圖、(2) 5.1MB 圖預期 413、(3) content-length 偽造、(4) 並發 10 個的 RSS 量測。

注意:(4) 用 process.memoryUsage()、CI 跑可能 flaky、加 retry x3。

package.json +1-0config

為什麼改:沒新增 dep、只把 sharp 從 ^0.32 鎖到 ^0.33、0.32 的 stream API 有個記憶體洩漏 issue 已修。

請特別 review

三個我自己也不太確定的點

  • middleware 順序size-limit 是不是放對位置?我目前掛在 app.ts L23、在 multer 前。但跟 auth 比、是 auth 先還是 size 先?我選 size 先(不浪費資源在驗 token)、但有 case 是 unauth 不該知道 413 嗎?
  • 新建 HttpError class:應該重用 AppError 還是新建?我選新建(不改現有 contract)、但長遠看是不是該統一?
  • 記憶體量測 test:用 process.memoryUsage() 在 CI 容易 flaky、我加了 retry x3 + 30% tolerance。覺得合理嗎?還是該 mock 掉這個 test 改放壓測 suite?

測試 / risk

跑過的測試

  • npm test ✓(86 pass / 0 fail)
  • npm run test:integration
  • local benchmark 5MB JPEG x 100 次、p95 410ms、無記憶體洩漏

沒測到的

Risk:沒測 PNG / WebP / GIF 大檔。sharp 對不同格式 stream 行為理論上一致、但 GIF 的 multi-frame 可能例外。建議 staging 跑一輪 mixed format upload 再 merge。

Rollback 計畫

單 commit、可乾淨 revert。size-limit.ts 是新增 file、revert 不影響其他 handler。processor.ts 改動有單元測試覆蓋、revert 跑舊 test 應綠燈。

叫 AI 給你一個

把你的 PR 資訊替換到下面 prompt 的 [括號]、貼進 Claude / ChatGPT / Gemini:

我要你幫我把一個 PR 寫成「給 reviewer 的導讀頁」、單檔 HTML。

PR 資訊:
- 標題:[PR 標題]
- 動機(為什麼開這個 PR):[ 1-2 段、含背景 / incident / user request ]
- 改的檔案 + diff:[ 貼 git diff --stat + 重點 diff、或描述每檔做了什麼 ]
- 我自己不確定的點:[ 列 2-3 條想請 reviewer 特別看的 ]
- 跑過的測試:[ 列測試結果 ]
- 沒測到的 / risk:[ 已知 risk + rollback 計畫 ]

請輸出一個單檔 HTML、結構:
1. Hero 標題 + 動機段
2. Before / After 切換 tab、顯示關鍵 code 對比 + 數字 delta
3. 一檔一檔的 collapsible card(點開看為什麼改 / 注意什麼)、依重要度排序
4. 「請特別 review」段、列我不確定的點
5. 測試 / risk / rollback 段
6. 左側固定 TOC、scroll 時 active 章節高亮
7. inline CSS / JS、不用任何 framework / CDN
8. 一個 .html 檔、繁體中文 UI label(code 內容保留英文)
9. mobile 320px 起跑得起來、窄螢幕 TOC 變上方 pill 列

直接給我可以右鍵存檔的完整 HTML。