There is a file in our codebase whose entire job is to hold twenty four strings. It has a longer doc comment than it has code. Here is why it exists, because the mistake that produced it is extremely easy to make and almost invisible once made.
The setup
lib/games/constants.ts is a large module. Around 4,400 lines. Most of that is GAME_LIBRARY, a catalogue of every practice game: names, descriptions, instructions, scoring configuration, the lot. It is the kind of module that grows one provider at a time and never gets read end to end.
It also exported the list of provider slugs:
export const ALL_PROVIDERS = Object.keys(PROVIDER_META) as AssessmentProvider[];
Correct, derived from a single source, exactly what you would write.
Now, somewhere else entirely, a small client component mounted in the root layout needs to know whether a URL segment is a known provider slug. It imports ALL_PROVIDERS.
That import pulls in lib/games/constants.ts. Which pulls in GAME_LIBRARY, because ALL_PROVIDERS is derived from a sibling object in the same module and the bundler cannot prove the rest is unused once the module has any side effect or any shared reference.
The component is in the root layout. So roughly 100 KB of game metadata landed in the first load of every page on the site. The blog. The pricing page. The login page. Pages with no games on them at all.
Why nobody sees this
This is the part worth internalising. The bug is invisible through every normal review path.
The import line looks perfect. import { ALL_PROVIDERS } from './constants' is the most ordinary line in the file.
Nothing is slow in development, where modules are served individually and unminified size is not a signal you look at.
The page works. There is no error, no warning, no failing test. Lighthouse gives you a worse number than you expected and you assume it is the images.
And the cost scales with a file you are not editing. Somebody adds a provider to GAME_LIBRARY, which is entirely reasonable, and the login page gets heavier. There is no point at which a reviewer sees a diff that makes this visible.
The fix is boring, which is the point
const PROVIDER_IDS: Record<AssessmentProvider, true> = {
'arctic-shores': true,
hirevue: true,
pymetrics: true,
sova: true,
shl: true,
// ...twenty more
};
/** Every assessment provider covered, in display order. */
export const ALL_PROVIDERS = Object.keys(PROVIDER_IDS) as AssessmentProvider[];
Its own module, importing nothing but a type. Callers that want slugs now import twenty four strings instead of a catalogue.
The doc comment records why, because the file is otherwise obviously redundant and the obvious "cleanup" is to merge it back:
ALL_PROVIDERSused to be derived insidelib/games/constants.ts. That was correct but expensive for the browser:constants.tsalso holdsGAME_LIBRARY, which is the bulk of a 4,400-line module, so ANY client component that wanted the list of provider slugs pulled the entire game catalogue into its bundle.
A file that exists for a bundling reason must say so, or it will be deleted by someone tidying up. Tree shaking is a best effort optimisation, not a guarantee, and the reliable way to not ship something is to not import the module that contains it.
Why a Record and not an array
The natural shape for a list of slugs is an array literal. We used an object keyed by the union type instead:
const PROVIDER_IDS: Record<AssessmentProvider, true> = { /* ... */ };
Record<AssessmentProvider, true> forces this object to name every member of the union declared in types.ts. Add a provider to the type and forget to add it here, and you get a compile error rather than a slug that quietly stops being recognised at runtime.
With const ALL_PROVIDERS: AssessmentProvider[] = ['shl', 'aon', ...], a missing entry type checks perfectly. The array is simply shorter than you thought. The provider's page still renders, because it is a static route; it just stops appearing in the sitemap, or stops matching the URL detection, or drops out of a count on the landing page. Silent, partial, and discovered by a user.
The true value is meaningless. The exhaustiveness is the entire feature.
The order is load bearing, so a test says so
The ORDER of these keys is load-bearing: it is the order providers are listed and numbered on the landing page. It matches the key order of
PROVIDER_META, which is what this list used to be derived from, and__tests__/games/provider-ids.test.tsfails if the two ever diverge.
This is the piece I would most want to steal.
Splitting the list out of the module it was derived from created a new failure mode: two hand maintained lists that used to be one derivation. The derivation guaranteed the order matched. Now nothing does, except that a test reads both and asserts they agree.
That is the trade you are making every time you duplicate something for a performance reason. You gain the bundle saving and you take on a drift risk. A comment saying "keep these in sync" does not discharge that risk. A test does, and it costs about six lines.
And Object.keys() order is not an implementation detail here, it is a documented guarantee for string keys: insertion order, as long as none of them look like array indices. Provider slugs never will.
How to find yours
The generic version of this problem: a small, cheap value exported from a large, expensive module, imported by something that runs everywhere.
Two things that find it quickly:
- Look at what your root layout transitively imports, client side. Anything in there is in every single page's first load, including pages that are otherwise static.
- Open the bundle analyser and sort by what appears in the most entries rather than by what is largest. The 400 KB chunk on one route is usually known about. The 100 KB chunk on all forty routes is the one nobody has noticed.
Then check whether the small thing can move to its own module. It usually can, and the module usually looks silly, and that is fine.
The pages this was costing
The twenty four providers in that file each get a page under the games directory, which is the surface that legitimately wants the full catalogue and is welcome to load it. The pages that were paying for it and getting nothing are the ones with no games on them at all.
If you want a two minute version of this exercise on your own site: open your login page with devtools, sort the network panel by size, and look for a JavaScript chunk whose name has nothing to do with logging in.
Top comments (0)