A product page loads twenty separate <script> tags — an application bundle, a polyfill file, an analytics tag, an A/B testing tag and a chat widget among them. How do you decide what loading treatment each one gets, and what does each decision actually move?
answer
- sort the list, do not blanket-apply
- ask what depends on each script
- before paint, after paint, on demand
- deleting beats deferring
basics
~20 sTriage every script by what depends on it: render-critical code stays inline and tiny, anything the first paint does not need is deferred, anything used only after an interaction loads on demand, and unused tags get deleted.
solid answer
~50 sI sort them into buckets by what actually depends on each one, rather than applying one policy to all twenty. Very little truly must run before the first paint — a theme or anti-flicker snippet, maybe a feature-flag read — and that stays inline and tiny. Everything the first render does not need, including the app bundle on a server-rendered page and most tags, gets a non-blocking treatment so its download stops competing with the HTML and CSS the paint depends on. Anything used only after an interaction — chat, video player, an export dialog's code — loads at that moment or during idle time, so a visitor who never uses it never pays. Then the harder question for every remaining tag: does anyone still read its data? Deletion is the only change that improves paint and responsiveness at once, because deferring moves execution cost, it never removes it.
code
html · 12 lines<!-- runs before paint: keep it tiny and inline -->
<script>
document.documentElement.dataset.theme =
localStorage.getItem('theme') || 'light';
</script>
<!-- needed on the page, not for the paint -->
<script defer src="/app.js"></script>
<!-- fetched only when the feature is used -->
<button id="open-chat">Chat</button>
<script defer src="/chat-loader.js"></script>go deeper
Be ready to say what the page needs in order to show something, and that scripts not needed for that should not hold it up. Naming one script you would load only when the user clicks something is enough here.
Explain the buckets and, crucially, which metric each choice moves: non-blocking loading helps the paint, while only shipping or running less JavaScript helps responsiveness. Interviewers expect you to reason per script, not per policy.
Show you would build the evidence first — bytes, execution time and an owner for each script — and propose per-script changes with the tradeoff named. Demonstrate that you know deletion is the strongest available move and how you would get agreement for it.
Own the framing that a page's script list is a shared budget with real owners. Talk about how you keep the triage from being a one-off cleanup: who decides, what a new script must justify, and what you trade away when a revenue-carrying tag is the expensive one.
## What the question is really testing Anyone can recite that `async` and `defer` exist. What an interviewer wants here is whether you can take a real page's script list and say, script by script, what it should be doing and why — in terms of what the user experiences. Script triage is decision work, and the decision is driven by dependency, not by which attribute you happen to like. ## Start from dependency, not from the attribute For each script, ask three questions in order: 1. Does anything the user sees at the first paint depend on this running? 2. Does anything on this page depend on it at all, at any time? 3. Does anyone actually consume what it produces? Those three questions sort twenty scripts into four buckets, and the bucket determines the treatment. ## Bucket 1 — must run before the first paint Very little qualifies. Realistic members: a snippet that sets a theme class so the page does not flash the wrong colours, a locale or feature-flag read that decides which variant of the markup is shown, an A/B testing hook that must apply before content is visible. These are inlined, because a separate request for 300 bytes buys a round trip you cannot afford at that moment, and they are kept genuinely tiny. Everything about this bucket is a cost you accept reluctantly: the bytes ride along in every HTML response, they cannot be cached on their own, and the paint is delayed by exactly their execution time. ## Bucket 2 — needed on this page, but not to paint it The application bundle on a server-rendered page, form validation, the analytics tag that must record a page view, the code for components already on screen. These get a non-blocking treatment so their download runs alongside document parsing instead of ahead of the CSS and markup the paint needs. Be precise about what this wins: it improves the paint metrics because the network is no longer contended and rendering is no longer gated, and it does approximately nothing for the execution cost, which now lands immediately after parsing instead of during it. ## Bucket 3 — needed only when something happens Chat widgets, video players, rich-text editors, print and export code, the module behind a rarely-opened modal, consent-gated vendor tags. Load these at the moment they are needed — on the click, or optimistically on hover or focus so the fetch is already in flight — or during idle time once the page is usable. This is the only bucket where the cost can be zero for most visitors: someone who never opens the chat never downloads or runs it. The tradeoff is a small delay the first time the feature is used, which is usually invisible if you start the fetch on intent rather than on the click itself. ## Bucket 4 — nobody needs it On a page that has been alive for a few years, several tags land here: a vendor whose contract ended, a second analytics tag added by a different team, a polyfill file for browsers you no longer support, a whole SDK loaded to call one function. Deletion improves everything at once — fewer bytes, fewer connections, less main-thread work, one fewer thing that can fail. Saying this out loud is what separates a triage answer from an attribute answer. ## What each choice moves - **FCP** responds to what competes with, or gates, the first render. Moving scripts out of that path is the single biggest lever. - **LCP** responds to the same contention, plus one special case: if the largest element is drawn by JavaScript, your script strategy *is* your LCP strategy, and no attribute change fixes that — only shipping the element in the HTML or shipping less script before it does. - **TBT and field responsiveness** respond to execution, which is a function of how much JavaScript runs, not of when you told it to start. Deferring changes *when*, never *whether*. ## Doing it on a real page Build the table before you argue: per script, its owner, its purpose, transferred bytes, main-thread execution time, and whether the first render depends on it. Sort by execution time descending. The top three usually make the case for themselves, and a table with an owner column turns "we should have less JavaScript" into five specific conversations with five specific people. ```html <!-- bucket 1: must run before paint, inline, tiny --> <script>document.documentElement.dataset.theme = localStorage.getItem('theme') || 'light';</script> <!-- bucket 2: needed, but not for the paint --> <script defer src="/app.js"></script> <!-- bucket 3: fetched when the feature is used --> <button id="open-chat">Chat</button> ``` ## Traps to avoid Blanket "defer everything" is not a strategy: you moved a pile of work to a slightly worse moment and told yourself the green lab paint score meant you were done. "Analytics has to be first or we lose data" is almost always false for modern tags, which are built to record late. And a script's file size is not its cost — a small file of dense initialisation work can occupy the main thread longer than a large file that mostly sits there.
- You defer the application bundle on a page that renders entirely from client-side JavaScript. What have you actually gained?Very little. If there is no server-rendered content, nothing meaningful can paint until that bundle runs, so the bundle is render-critical whatever attribute it carries. You have only stopped it from competing with the stylesheet. The real levers there are sending real HTML for the first screen and shipping less JavaScript before the first render, not the loading attribute.
- How do you decide whether a feature's code should load during idle time or wait for the user's interaction?By likelihood and weight. If most visitors use the feature and its code is modest, warm it once the page is idle so the interaction feels instant. If it is heavy or rarely used, wait — but start the fetch on intent, such as hover or focus on the control, so the download overlaps the user's decision instead of the click.
- A stakeholder insists a tag must load before anything else so no data is lost. How do you handle that?Ask what specific event they are worried about losing and check the vendor's documentation, because most tags are designed to record page views recorded late and to flush queued events when the page is hidden. If a genuine measurement gap exists, quantify it against the paint delay the blocking load imposes on every visitor, and let the owner of the number choose with both costs in front of them.
Triaging scripts is closer to packing for a flight than to tuning an engine: the question for each item is who needs it and when, and the fastest bag is the one with fewer things in it.
saying these in an interview costs you the question
- Answers with 'just put defer on everything' and stops there
- Treats deferring as removing cost rather than moving it
- Never asks whether a tag is still needed at all
- Assumes a small file means a cheap script
- Insists analytics must load before the page renders