Keep article bodies out of the homepage bundle
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
// 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
| Artifact | Contents | Consumer |
|---|---|---|
| articleCatalog.generated.ts | Titles, summaries, dates, tags, and other listing fields | Homepage, Journal, and listing/search UI |
| articleContent/<slug>.generated.ts | Full Chinese and English versions of one article | The requested article page |
| articleContentLoaders.generated.ts | Slug-to-literal-import functions | The content loader |
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.
// 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.
Reproduce the fixed version and inspect the production build
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.1Use a separate checkout so the walkthrough does not disturb an active branch. The fixed revision can differ from today’s website; it provides a shared reference. Inspect the generated catalog, then the per-article source modules, and finally dist/assets. Separate source files alone do not establish that the production output preserves the intended boundaries.
In the preview server, 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. 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.
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