Your team is debating whether to adopt a rule that every mutation in a Next.js App Router app must work without JavaScript. How do you make that call, what does the rule actually cost, and how would you keep it true over time?
answer
- reframe away from NoScript users
- everyone is unhydrated at first
- chunks fail more often than people think
- make the compliant pattern the easy one
- an unverified guarantee decays
basics
~20 sDecide per surface, not globally. The rule's real payoff is the gap before hydration and cases where the bundle fails, not users who disable JavaScript. Mandate it for the critical funnel, allow reasoned exceptions for interactions with no native equivalent, and verify it in CI or it silently rots.
solid answer
~50 sI would reject both absolutes. The honest justification is not the tiny population that disables JavaScript — it is that every user is a no-JavaScript user for the first seconds of a cold load, and some fraction never get the bundle at all because of a failed chunk, an aggressive network, or an old device. So the value is highest on entry points: auth, search, checkout, the primary create and update paths. It is lowest on interactions with no native equivalent — drag-to-reorder, canvas, autosave. The cost is real: mutations must be form-shaped, the fallback path is a full navigation with no optimistic UI, and you carry a second code path to keep honest. My rule would be: form actions are the default and require no justification; handler-only mutations require a stated reason in review; and a small automated pass exercises the critical flows with scripting disabled, because an unverified guarantee is not one.
go deeper
Understand that the argument is about resilience during the seconds before a page becomes interactive, not about a niche group of users who turn scripting off.
Be able to state both sides concretely: what the fallback path loses (navigation, optimistic UI, returned state) against what it buys (a mutation that works from first paint).
Show how you would tier surfaces, keep the compliant pattern the default so compliance is passive, and name the verification that stops the property from decaying unnoticed.
Own the policy end to end — size the affected population with real telemetry, set the exemption process so it stays visible, and be prepared to narrow or widen the rule as the evidence changes.
## Reframe the question before answering it The framing "do we support users with JavaScript disabled" invites the wrong answer, because that population is genuinely small and everyone in the room knows it. The framing that produces good decisions is: **for how long, and how often, is our app in a state where our JavaScript is not running?** Three populations sit under that question: 1. **Everyone, briefly.** Between first paint and hydration, no handler in the app works. On good hardware that is imperceptible; on a mid-range phone over congested mobile data it is seconds, and it lands precisely when an impatient user is clicking. 2. **A minority, permanently.** Chunks fail. Corporate proxies and extensions block scripts. A parse error in one bundle can take out interactivity for a whole route. This is a small percentage that shows up as "the button did nothing" tickets no one can reproduce. 3. **Non-browser consumers.** Crawlers, link previewers and text-mode tools never execute your handlers. Usually irrelevant for mutations, occasionally relevant for search-type forms. That reframing also sets the ceiling on the argument: this is a resilience property, not an accessibility mandate and not a moral position. ## What the rule actually costs Be specific, or the debate turns into slogans. - **Shape constraint.** Mutations must be expressible as a form posting to an action. Most CRUD is; some interactions are not, and forcing them produces contorted markup. - **A second experience to reason about.** The fallback is a full-page navigation: scroll resets, transient client state is lost, returned action state cannot reach the user, so results have to be communicated via redirect, persisted state or cookies. - **A maintenance surface.** The property is invisible in code review — nothing fails when someone converts a form to a click handler. Without verification it decays within a couple of quarters, and you end up claiming a guarantee you do not have, which is worse than never claiming it. - **Design pressure.** Multi-step wizards, dependent fields and inline validation are all easier when you assume scripting. Preserving a baseline sometimes means a simpler flow. Against that, the cost of the *default* case is close to zero: `<form action={...}>` is not harder to write than a click handler, and it often removes a Client Component boundary, which is a bundle win rather than a tax. ## The decision I would actually make Tier the surfaces: - **Must hold the baseline** — anything that blocks a user from getting in or transacting: sign-in, sign-up, password reset, search, checkout, and the primary create/update/delete of the core object. These are entry points, often the first interaction after a cold load, and the ones where failure is unrecoverable for the user. - **Should, by default** — ordinary secondary CRUD. Use form actions because that is the path of least resistance; do not litigate it. - **Exempt with a reason** — interactions with no native equivalent, and dense authenticated tooling where the user is already deep in a hydrated session. The critical move is making the compliant pattern the *easy* one. If form actions are the shape everyone reaches for, compliance is a side effect. Rules that require extra effort to follow lose to deadlines. ## Keeping it true An unverified guarantee decays silently, so pick a mechanism proportionate to the claim: - A small end-to-end lane that runs the tiered-critical flows with scripting disabled. It is a handful of tests and it converts an aspiration into a check. - A review convention: a handler-only mutation needs one sentence saying why. Not a veto — a prompt. - Occasional manual passes under heavy network throttling, which catch the hydration-gap version of the same bug that a scripting-disabled test does not model well. ## How to present it Avoid the two failure modes. "We support NoScript users" is unpersuasive and probably untrue in the aggregate. "Nobody disables JavaScript, so it does not matter" ignores that every session begins without JavaScript and some never leave that state. The defensible position is narrow and measurable: our critical flows work from first paint and survive a failed bundle, and here is the test lane that proves it.
- What evidence would you gather before setting the policy?Field timing for how long the gap between first paint and interactivity actually is on real devices, plus error telemetry for failed script loads on the critical routes. Together those size the affected population honestly. If the gap is tens of milliseconds and chunk failures are vanishing, the rule shrinks to the entry points; if either is significant, it broadens.
- Does this policy conflict with heavy use of Client Components?Less than people expect. Keeping mutations as form actions often removes the reason a component had to be a Client Component at all, which reduces bundle size rather than fighting it. The genuine conflicts are interactions with no native equivalent, and those are exactly the ones the policy should exempt rather than contort.
- How do you stop the exemption clause from swallowing the rule?Require the reason to be written down at the call site or in review, and audit the exemptions periodically rather than gating each one. Visibility, not permission, is what keeps the count honest. If exemptions grow steadily on the tiered-critical surfaces, that is the signal to revisit the policy rather than to keep granting them.
saying these in an interview costs you the question
- Justifies the rule solely by users who disable JavaScript
- Dismisses it because that population is tiny
- Declares a blanket rule without tiering surfaces
- Claims the guarantee without any verification lane
- Assumes the fallback experience must be identical