title: "修復棕地 SPA 中細微的快取不一致:一個務實的解法"
published: true
description: "我們如何在 DEV 的部署過程中,消除那些細微的樣式表快取異常,而無需大規模重寫。"
tags:

  • webdev
  • architecture
  • webperf
  • rails
    ai_disclosure: some_ai

公開開發(building in public)通常意味著談論閃亮的新功能,但在成熟的正式環境應用程式中,最關鍵的工程工作通常是 棕地問題解決(brownfield problem solving)——在多年累積的程式邏輯和/或正式系統決策之上持續迭代。

DEV(由開源 Forem 程式碼庫驅動)上,我們有一套混合式架構,結合了 Rails 伺服器端渲染、Fastly 邊緣快取,以及輕量級的前端導覽(透過 InstantClick)。這套架構可提供低於 100ms 的頁面切換,但局部頁面替換搭配激進的邊緣快取,也帶來了微妙的部署挑戰。

我們推出了一個修正 PR #23789,用來協助解決 Web 導覽中本質上就很脆弱的快取不一致問題。這個問題幾乎從 DEV 一開始就存在,我們過去也曾為此做過幾次不太穩定的修補,但我認為這次確實是朝正確方向邁出了一步——儘管稱不上是完全完美的修復。


問題:跨部署的快取不一致

當我們部署新的 CSS 更新時,Rails 會產生新的資產摘要雜湊值(例如,views-v2.css 取代 views-v1.css)。

  1. 完整頁面載入: 使用者載入更新後的首頁。瀏覽器接收完整 HTML,並在 <head> 中載入最新的 v2 樣式表。
  2. 站內導覽(局部頁面替換): 使用者點擊文章連結。InstantClick 不會執行完整的瀏覽器重新載入,而是在背景請求該文章,並只將內層的 #page-content 容器替換進現有的 DOM。
  3. 邊緣快取陷阱: DEV 會使用 surrogate key 在 Fastly 上大幅快取文章 HTML 片段。許多文章是在最新部署之前就已被快取,這代表快取中的 HTML 片段仍然引用 v1 樣式表。

為什麼之前的解法很脆弱

為了避免頁面以過時的 CSS 呈現,我們先前加入了前端邏輯,去檢查進入頁面預期的樣式表路徑,並動態替換 DOM 中的 <link rel="stylesheet"> 元素。

實際上,這種做法很脆弱:

  • 當從最新的首頁(v2)導覽到一個較舊、且被邊緣快取的文章(v1)時,前端腳本會比較 currentHref (v2) !== expectedHref (v1)
  • 它會假設傳入的頁面代表的是「目標」狀態,並觸發 DOM 非自願降級v1 樣式表。
  • 如果 v1 的資產檔案在部署後已被清除,瀏覽器就會因為 404 而失敗;如果載入成功,新 UI 元件又會因為更新後的 CSS 在會話中途被移除而突然壞掉。
  • 嘗試在 <head><body> 之間非同步替換多個樣式表標籤,會引入 CSS 層疊順序的競態條件,以及無樣式內容閃爍(FOUC)。

解法:重新利用站內導覽參數

我們沒有把這件事當成前端 DOM 變異問題來處理,而是意識到它本質上是一個 快取分區(cache partitioning)問題

多年來,Forem 一直悄悄地在背景 AJAX 請求中傳遞一個內部查詢參數——?i=i——讓 Rails 知道要回傳輕量級的部分版型,而不是完整的 HTML 文件外框。

PR #23789 中,我們重新利用了這個參數:

  1. 合併樣式指紋: 在初次完整頁面載入時,Rails 會根據核心樣式表(minimalviewscrayons)的合併摘要計算出一個可重現的 10 字元雜湊,並將其以 data-style-fingerprint 的形式放在 <body> 上:

    <body ... data-style-fingerprint="cc9ed033eb">
  2. 為站內導覽加上參數: 當 InstantClick 預載或取得某個連結時,它會讀取目前工作階段的指紋,並將其附加到內部網址上:

    https://dev.to/user/post?i=cc9ed033eb

    (如果 data-style-fingerprint 不存在——例如在部署切換期間開著的分頁——就會優雅地退回到 ?i=i。)

  3. 在 Fastly 上自然進行快取分區: Fastly 的邊緣設定已經將 i 列入安全參數清單。由於查詢參數是快取鍵的一部分:

    • v2 工作階段的使用者請求 /post?i=v2_fingerprint 時,Fastly 會檢查是否有快取的 v2 片段。
    • 如果沒有,Fastly 就會向 Rails 取得一個以 v2 樣式編譯出的新鮮片段。
    • 舊的 v1 快取片段會被直接略過,並在之後自然過期。
  4. 刪除 DOM 替換邏輯: 有了快取分區之後,傳入的局部片段就能保證與目前 DOM 的樣式表版本一致。我們完全刪除了前端樣式表替換腳本。瀏覽器中作用中的 <link> 標籤在整個使用者工作階段中都保持靜態不變。


棕地開發的心得

  1. 在快取層解決,而不是 DOM 層: 嘗試在前端 JavaScript 中協調 DOM 變動、非同步 CSS 載入,以及層疊優先順序,幾乎總是比直接讓邊緣快取一開始就提供正確版本的 HTML 更脆弱。
  2. 重新利用既有原語: 我們不需要新的路由器、邊緣運算重寫,或自訂 HTTP 標頭。重新利用現有的 ?i=... 內部參數,就讓我們以零額外基礎設施成本取得了精準的快取鍵。
  3. 務實改良勝過完美重寫: 這個修正並不會神奇地阻止滾動部署期間所有理論上的邊界案例,但它確實解決了最具破壞性的問題。系統現在沒那麼脆弱了,而且我們也擁有了一個清楚、可預測的模式可以繼續建立在其上。

編碼愉快!


原文出處:https://dev.to/devteam/fixing-delicate-cache-mismatches-in-a-brownfield-spa-a-pragmatic-solution-dk9


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝16   💬4  
227
🥈
我愛JS
3
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登