TL;DR 懶人包:
用 AI plan mode 協助高複雜的改動,作為開發者必須做哪些事情,如何分 phase 提高可驗證性、可測試性,如何在 lazy loading 雙向載入任務中做技術選擇 trade-off,除了開發者的角色,作為一個 product owner 更是影響整個 product 發展、性格特色的主動推手,不應放掉個人在 AI plan mode 中的主導地位。文末最後做個簡單的 retrospective,要接近更自動化的 AI harnessing,還能加強哪些事。
延續 上一篇 談 STORY 頁 Timeline 從 client-side 運算 shift-left 到 server-side 的重構,這次的主題是雙欄(同步捲動的 timeline / events)的新 feature: lazy loading 雙向載入。
1. 雙向 lazy loading 需求與起點
現況兩欄都是 forward-only、append-only 的 lazy loading:只能往下捲、一頁一頁往後接。這次新的需求很單純兩條:
- 可以直接跳到某年份(例如 1900),而不需要先把 1900 之前的所有 event 與 timeline 都載入。
- lazy loading 要上下雙向,目前只有往下。
聽起來像兩個 feature,但它們其實動到同一件事,也就是 loading contract,好比要抓哪一段、往哪個方向抓、邊界(還有沒有更多)怎麼判定。這個觀察,決定了後面所有的排序。
2. 分 phase? Why 與 How
phase A 與 B 原本是一整包,但基於可驗證性,我請 AI 拆成 A(核心)/ B(優化),讓改動變得比較容易掌控,提高可驗證性、可測試性。
我目前是依據以下 2 條原則:
- correctness 與 optimization 分開:能讓功能「正確跑起來」的,跟「跑得更省」的,不要綁在同一個 Phase。
- 每個 Phase 可獨立驗證、可獨立上線。
拆分細節:
| 階段 | 內容 | 切分理由 |
|---|---|---|
| Phase A | windowed loader:跳年份 + 雙向 + correctness(epoch guard、anchor windowing、prepend viewport補償) | 兩個核心需求 + 必要正確性,面積小、好測 |
| Phase B | DOM eviction、可快取 GET、AbortController、scroll sync forward-only fallback 清理(改方向感知) | 純 optimization,可在 phase A 穩定後獨立疊加 |
3. 技術選擇 trade-off
由下表可以看到,多數決定不是「哪個比較強」,而是「哪個對現在這個規模剛好」。除了hasMore 判定這種單純技術優勢的選擇之外,比如決定年份跳轉行爲算是我個人主觀的決定,也正是這些決定帶動了整個 product 的走向,我希望透過在這 side project 累積的無數抉擇、經驗,使我在之後的開發與設計上,有更敏銳的決策力。
規劃過程真正該 put effort 的是這些抉擇。每一條都是「Before vs. After」之間的取捨:
| 問題 | 選項 | 抉擇與原因 |
|---|---|---|
| 決定年份跳轉行爲 | replace window / keep + bridge | 選 replace:跳轉時把整個 window 換成「以目標年為中心」的新一段、丟掉舊區段,所以跳 1900 只載 1900 附近,不會把 1900 之前全部拉進來;之後上下捲再從這個新 window 的邊界繼續接。另一個選項 bridge(保留舊區段、再補載中間)雖舊區段省了重載,但和下行講的 eviction 相衝突 |
| DOM 大小 | 無限成長 / eviction 固定 | 做 eviction,但延到 B 階段做。「省記憶體」這個優化雖重要,但它讓狀態複雜度近乎翻倍。 目前機制:window 上限約保留 120 筆 events(timeline 是 300 年),超過就從「遠離 viewport 的那一端」往回砍,如果使用者捲回去也能再載回 |
| prepend viewport補償 | 錨定真實元素 / scrollHeight delta | 用 scrollHeight delta:往上插入內容會把畫面往下擠,只要量出「容器高度多了多少」、再把捲動位置補回去,畫面就不動。新增的高度全在畫面上方,這個量法剛好精確,不必去錨定某個真實元素 |
| hasMore 判定 | length === limit heuristic / limit + 1 | 抓 limit + 1。剛好等於 limit 時 heuristic 會誤判「還有更多」,多取一筆只用來定旗標就精確 |
| 原生捲動錨點 | 依賴 overflow-anchor / 關閉並手動補償 | 關閉 browser native behavior。它與手動補償並存會雙重作用造成抖動 |
server action vs. client fetch + cache
「events 傳輸」值得挑出來講,因為它是 lazy loading 的核心,同時牽動載入、快取、取消三件事。
以下是在 before & after 的選擇:
- 在加 lazy loading 之前: server action
- 在加 lazy loading 之後: client fetch
server action(Next.js v14 up)與傳統 fetch + cacheable GET 比較:
| 面向 | server action (server function) | fetch + cacheable GET |
|---|---|---|
| HTTP / CDN 快取 | 不可:是 POST,無法被 CDN / 瀏覽器快取 | 可:帶 Cache-Control: s-maxage,跨使用者與回跳命中 |
| 開銷 / 並發 | 走 RSC action 佇列、傾向序列化,rapid 操作下延遲較明顯 | 一般 GET,瀏覽器可並發、可預取(hover prefetch) |
| 適合場景 | 免 JS mutation / form 應用 / 敏感資料&金流 / 搭配 RSC 觸發 revalidate | 可快取的讀取 / 會被重複或多人請求 / 需要取消或預取 |
Before - server action:取決於一次載入速度比較快,避免 water fall 瀏覽器 single thread 載入卡住。並顧及 SEO。
After - client fetch:因為加了 lazy loading 功能,勢必要取捨掉一部分 SEO,但為了整頁體驗與速度,並兼顧瀏覽器(這個分頁)避免佔用太多記憶體(至少保證 1GB 以下),改為可 cache 的 client fetch。
分階段: 在 phase B 才改為 server action → client fetch,因為這部分算是 optimization,不影響「雙向 lazy loading」功能驗收,所以在 B 實作。
4. 與 AI 協作的要點
lazy loading 這 feature 從 plan 到實作的過程中,關於「和 AI 協作」我聚焦在兩個點:
-
加法:在第一版 plan.md 產出時,完全沒提到 API 的改動,雖然在其他情境上可能會是個 scope 清楚的計畫,但在這次的情境,重寫一個適合lazy loading 的 API完全是一個「該納入考量的點」,作為開發者需要注意到這種缺漏,並提出針對性的架構討論,能夠把 AI 不會主動提的優化拉進新 plan 裡。
-
減法:AI 兩次把自己的 plan 做得偏滿,我除了做清楚的架構 review 之外,判斷 plan「有沒有 over-design」,能讓 plan 有效收斂。
好比,做 events 的「eviction 功能」時,有邊界抖動的風險(往下捲 → 載入 → 超過上限 → trim 頂端 → hasMoreUp 變 true → 如果頂端 sentinel 剛好又在觸發範圍 → loadUp → 又超限 → trim 底端 → ...),此時解法之一是「使 window 夠大 (目前是 120 筆 events)」,就不用另外杞人憂天地寫 cooldown(短暫 lock 反向捲動)或距離防守(要 trim 的內容離 viewport > 某保守高度),後者我認為是 over-design。
5. 回顧檢討:應該建立 harnessing 機制
這次的重構雖然透過人工 review 補齊了 API 改動並剪掉 over-design,但本質上仍屬於依賴個人經驗的「手動監工」。若要降低後續審查的認知負荷,開發流程可以進一步自動化:
- 將架構限制寫入專案 context:
將記憶體上限(1GB)、trade-off 偏好(如 replace > bridge)與設計守則(如「防護 lock 不得超過兩層」)直接寫進 architecture decision decords (ADR) 即CLAUDE.md。讓 AI 在 plan 階段就自動帶入這些限制條件,減少產生無效 plan 的次數。 - 以 invariant 測試作為 AI 實作的邊界:
在 phase A 開始前,要求 AI 根據 spec 先寫出不變量測試(例如:檢測 sentinel 是否移出觸發區、viewport 位置是否穩定)。讓 CI 測試扮演防護網,確保 AI 在實作 phase A 時不會邊界溢出去踩到 phase B 的範疇。 - 將 review 教訓寫回 prompt / memory 系統:
每次發現 AI 的思考盲點(如漏掉 API 重構、忘記清理 event listener),除了在 plan 修正,也要同步更新寫入專案的 AI memory。確保同類型的架構缺漏不會在下一次的新 feature 重演。
這次的開發過程比較像「事後 review 幫 AI 補漏」,但轉向「事前建立 context 與 test 邊界」才能更接近效率化的 loop engineering,把個人架構能力轉化為可重複利用的工程機制。