ViteReact效能內容網站

Keep article bodies out of the homepage bundle

···5 min read

A homepage can display ten article cards while downloading every article body. Invisible content can still be present in the JavaScript bundle. When cards and article pages share an array containing full bodies, adding long articles may increase what the homepage fetches. The useful place to start is that dependency, before trying to optimize individual React renders.

The case study uses the Vite 7 / React 19 architecture at revision 1a6b177. Familiarity with imports and basic React routing is enough. By the end, you should be able to identify listing fields, locate the body-loading boundary, and inspect built files and network requests. This article does not claim a percentage improvement in load time or conversions.

Trace imports before counting visible cards

javascript
// A listing that imports complete bodies keeps that dependency.
import { articles } from './allArticles';
export const cards = articles.map(({ slug, title }) => ({ slug, title }));

The mapping returns only slug and title, but allArticles is still an input to the build. What the bundler can remove depends on static analysis and other consumers. Selecting two fields is not proof that bodies disappear. A separate data boundary is easier to inspect, especially when the full array is used elsewhere or selected dynamically.

Separate the catalog, bodies, and loader map

ArtifactContentsConsumer
articleCatalog.generated.tsTitles, summaries, dates, tags, and other listing fieldsHomepage, Journal, and listing/search UI
articleContent/<slug>.generated.tsFull Chinese and English versions of one articleThe requested article page
articleContentLoaders.generated.tsSlug-to-literal-import functionsThe content loader
The site’s article data boundaries

A build-time collector reads article sources and generates all three outputs. The catalog omits fields such as content and sources; each slug gets its own body module. The homepage can show a summary without owning the body. Authors maintain one source, avoiding a second manually synchronized title-and-date list.

typescript
// Reduced shape of the generated map, not runtime path construction.
export const articleContentLoaders = {
  'react-chinese-ime-onchange':
    () => import('./articleContent/react-chinese-ime-onchange.generated'),
};

The generated map uses literal paths so the build can see the target modules. Source discovery happens before the browser receives the loader list. There is a deliberate tradeoff: both languages of one slug live in the same module. Opening Chinese also retrieves that article’s English version. This separates articles, not languages; a language-level split would require a separate cost assessment.

Promise caching also caches a failure

The content loader caches the module promise by slug, then the localized result by locale:slug. Concurrent callers receive the existing promise. Preloading and rendering can therefore share application-level work. This does not guarantee exactly one HTTP request under every browser and deployment-cache condition.

A limitation in the inspected implementation matters: rejected promises stay in the maps. There is no failure-eviction branch, so this version does not implement automatic retries. Adding retries would require handling both the module and locale caches and distinguishing a missing slug from a failed network load. Calling the same function again may simply return the same rejected promise.

Read the fixed-version source, then inspect the production build

Download the three cited source files at revision 1a6b177 (ZIP)
bash
unzip article-code-splitting-1a6b177.zip
cd article-code-splitting-1a6b177
shasum -a 256 -c SHA256SUMS

The site repository is not public, so this kit holds only the three files cited above, byte-identical to revision 1a6b177. The fixed revision can differ from today’s website; it provides a shared reference. In scripts/generate-article-artifacts.mjs, find where the catalog drops content and sources, then the literal import map written per slug. Then read the two caches in src/articleContentLoader.ts. These files cannot build the site alone. Separate source files also do not establish that the production output preserves the intended boundaries, so check the build output on the live site.

On keeponfirst.com, disable the browser cache and reload the homepage with Network open. Record the JavaScript requests. Open an article directly and identify its body module, then open another article and compare. The live site runs the current revision, not 1a6b177, so chunk names differ; compare the boundaries, not the file names. Do not mix development-server requests with production output. File count alone is also misleading: splitting can create more files while changing which files each entry needs.

Example: the current load order for a direct article visit

Top green box: the homepage reads only articleCatalog, without content or sources. Four steps below: prerendered HTML shows the body; main.tsx awaits preloadArticlePage; Promise.all loads ArticlePage and articleContent/<slug>.generated together; createRoot runs once after both arrive. A note says a failed Promise also stays in the Map, so there is no automatic retry
Load order for a direct article visit, based on the repo as of 2026-11-05. Diagram drawn by KeepOnFirst.

The notes below follow the site code as of 2026-11-05, not 1a6b177. Line numbers refer to the files on that date.

The catalog fields are set in scripts/generate-article-artifacts.mjs, lines 51-59. listItem drops content, sources and four SEO fields. Lines 92-94 of the same file write one literal import per slug. This build produced 87 Chinese and 84 English articles.

On a direct article visit, src/main.tsx awaits preloadArticlePage (lines 16 and 20). src/articlePageLoader.ts, lines 7-14, uses Promise.all to load the page component and that article body together. createRoot runs only after both arrive. So the prerendered body is not cleared to a blank page first.

The failure cache is still there. src/articleContentLoader.ts, lines 14-16, puts the Promise in the Map with no delete-on-failure branch. From 1a6b177 to 2026-11-05, this file changed only one type line (checked with git diff).

Shared zh and en modules have one exception. Chinese-only articles have no English version, so their body module holds Chinese only (scripts/generate-article-artifacts.mjs, lines 82-84).

Lazy loading and readable initial HTML are separate checks

Articles also have a prerendering path that writes their bodies into initial HTML. Browser startup then loads the matching page and data. Neither result proves the other: a body chunk does not establish readable initial HTML, and complete HTML does not establish a lean homepage dependency graph. Inspect both paths separately.

A useful acceptance check asks four questions: does the homepage fetch unwanted bodies, does a direct article URL work, is the initial HTML readable without JavaScript, and are the referenced chunks available after deployment? These track reader-facing behavior more closely than the presence of dynamic import. For a few short posts, added requests and maintenance may outweigh the benefit; measure that case before adopting the pattern.

Related: how this site evolved from an SPA to prerendering

Sources