文章越多,首頁越重?把文章目錄與正文拆開載入
文章網站的首頁可能只顯示十張卡片,卻把所有文章的正文都一起下載。畫面上看不見,不代表程式包裡不存在。如果卡片和文章頁都從同一個含完整正文的陣列取資料,新增長文就可能增加首頁要取得的 JavaScript。這篇要解決的是這條依賴關係,而不是先替每個 React 元件加 memo。
範例以本站 1a6b177 版的 Vite 7/React 19 架構為準。你需要知道 JavaScript import 和基本 React 路由。完成後應能指出哪些欄位屬於列表、正文從哪裡載入,以及用產物和 Network 紀錄確認拆分是否有效。本文沒有提供載入時間或轉換率的改善百分比。
先追 import,而不是看畫面顯示多少
// A listing that imports complete bodies keeps that dependency.
import { articles } from './allArticles';
export const cards = articles.map(({ slug, title }) => ({ slug, title }));上面的程式只輸出 slug 與 title,但 allArticles 仍然參與建置。實際能移除多少資料取決於 bundler 能否靜態分析你的程式,不能把「map 只取兩個欄位」當成正文一定會消失的證明。尤其完整陣列還被別處使用,或正文在執行時才挑選,最好建立明確的資料邊界。
目錄、正文、載入表,各自只負責一件事
| 產物 | 內容 | 使用者 |
|---|---|---|
| articleCatalog.generated.ts | 標題、摘要、日期、標籤等列表資料 | 首頁、Journal、搜尋等列表介面 |
| articleContent/<slug>.generated.ts | 該篇的繁中及英文完整正文 | 當前文章頁 |
| articleContentLoaders.generated.ts | slug 對應 literal import 函式 | 正文載入器 |
本站在建置前收集文章原始檔,再由同一份資料產生上述三種產物。目錄會排除 content 與 sources 等全文資料;每個 slug 有自己的正文模組。這樣首頁可以顯示摘要,卻不用持有正文。作者仍只維護文章來源,避免手動維護另一份列表時忘記更新標題或日期。
// Reduced shape of the generated map, not runtime path construction.
export const articleContentLoaders = {
'react-chinese-ime-onchange':
() => import('./articleContent/react-chinese-ime-onchange.generated'),
};這份載入表把路徑寫成 literal,讓建置工具看得到有哪些模組。來源檔名可以自動發現,但瀏覽器拿到的是已產生的 import 清單。請留意另一個取捨:本站一個 slug 的中英文放在同一個模組,因此開繁中也會取得同篇英文。它隔離的是不同文章,不是不同語言;若長文翻譯佔比很高,才值得另外衡量是否拆成語言層級。
Promise 快取處理重複要求,也保留失敗
正文載入器用 slug 快取模組 Promise,再用 locale:slug 快取翻譯結果。兩個元件同時要求同一篇時,會取得已存在的 Promise。這能避免應用程式重複建立同一份載入工作,也讓文章頁預載與實際渲染共用結果。它不等於所有 HTTP 請求都必然只發一次,瀏覽器與部署快取仍有自己的行為。
原始碼還有一個值得明講的限制:rejected Promise 也會留在 Map,沒有失敗後刪除的分支。因此本文這個版本不提供自動重試。讀者若要加入重試,必須同時處理模組與語言兩層快取,並區分「不存在的 slug」與「網路載入失敗」;只在外面再呼叫一次同一個函式,可能仍拿到原本失敗的 Promise。
用固定版本重現,再看正式建置產物
git clone https://github.com/keeponfirst/keeponfirst-website.git
cd keeponfirst-website
git checkout 1a6b177
npm ci
npm run articles:generate
npm run build
npm run preview -- --host 127.0.0.1請在獨立目錄操作,避免切換你正在工作的分支。固定版本可能與線上內容不同,這正是為了讓檢查有共同基準。先看 src/generated/articleCatalog.generated.ts 是否只有列表所需資料,再開 src/generated/articleContent 觀察每篇模組。最後檢查 dist/assets,確認產物仍有文章邊界;只看到來源檔已分開還不夠。
在預覽網址開啟瀏覽器 Network,停用快取後重載首頁,記錄載入的 JavaScript。接著直接開一篇文章,對照新增的正文模組;再換另一篇,確認不是所有文章同時被載入。不要把開發伺服器的請求數與正式產物混在一起,也不要只看總檔案數:拆分通常會增加檔案數,重點是某個入口實際需要哪些檔案。
正文延後載入,初始 HTML 仍要能讀
本站文章另有預渲染路徑,會把正文寫入初始 HTML;瀏覽器啟動時再載入對應文章頁與資料。這兩件事不能互相代替:有正文 chunk 不表示爬蟲能在初始 HTML 讀到內容,有完整 HTML 也不表示首頁沒有帶著全站正文的 JavaScript。兩條路要分別檢查。
實際驗收可分四個問題:首頁有沒有取得不需要的正文?直接開深層文章能否成功?停用 JavaScript 後原始 HTML 是否有內容?重新部署後的 chunk 路徑能否取得?這四個答案比「已經用了動態 import」更接近讀者真正遇到的情境。若網站只有幾篇短文,拆分增加的請求與維護成本也可能不划算,先量測再決定。
延伸閱讀:本站從 SPA 到預渲染的演進