How to write JS that stays fast without turning every function into a micro-optimization

Marcus Webb

Marcus Webb

August 27, 2026

How to write JS that stays fast without turning every function into a micro-optimization

I have sat in reviews where a helper that ran once per click got a cache, a custom loop, and a comment about V8 inlining, while the page still hitching was a list of two thousand rows and a layout read in the scroll handler. We optimized the function because it was a function. The frame did not care.

Fast JavaScript in 2026 is mostly not clever JavaScript. It is JavaScript that does less work on the path the user can feel, and boring JavaScript everywhere else. I write frontend for a living. This is how I keep a codebase quick without turning every file into a contest.

Speed is a place, not a style

A click handler that does 200 microseconds of extra work is not your problem. A scroll handler that does 8 milliseconds of layout is. A navigation that parses 3MB of JSON is. A list that mounts a small application per row is. If you cannot name the place — the gesture, the route, the list — you are not optimizing. You are decorating.

I start with a reproduction on a mid-range phone, or CPU throttle if I am stuck on a laptop, and I record the gesture. Then I pick the longest task on that path. One. I do not open a file because a linter said a map could be a for-loop. The engine is better at that than our pride is.

The places that keep showing up are the same ones I wrote about when I listed the layout, list, and async mistakes that still jank. If you have not reserved height, windowed the list, or un-serialized the waterfalls, stop reading blog posts about hidden classes. You do not have a micro problem.

Developer pointing at a blurred performance flame chart with colleagues

Rules I actually follow

Don’t touch the DOM to ask a question you just caused. That is layout, not algorithms. Batch writes. Read once. Prefer CSS for the motion. This rule has saved more frames than every memo I have deleted.

Don’t give the main thread a novel. Parse less JSON on navigation. Send the list shape the list needs. Defer the blob. JSON.parse is real work. So is creating thousands of objects you will throw away after paint.

Don’t schedule work on the hot path that can wait. Analytics, session replay, a prefetch for a route the user has not aimed at, a “warm the cache” loop in requestIdleCallback that you accidentally put in the click. Idle is a courtesy. The click is a contract.

Don’t fight the framework’s render model until you have a trace. Memo, signals, compilers — they are tools for update cost. If the cost is mount count or layout, they will make you feel busy. I have shipped a “compiled” page that still stuttered because we mounted the world. Compilation is not a window.

Do write boring, copy-pasteable code in cold paths. A settings page that runs once can use a map, a filter, and a comment. I will not accept a typed ring buffer in that file. Future me has to change the settings. Future me is not a compiler.

When a micro-optimization is honest

Sometimes the trace points at a tight loop. Image processing in JS, a parser you should have left in WASM, a hot path in a canvas, a conflict resolver that runs per keystroke in a huge document. Then I will write the ugly loop. I will write a comment that names the trace, not a comment that names my cleverness. I will add a test that the result did not change. I will not spread the style to the rest of the module.

The honesty test: if I remove the optimization, does a user-visible metric move? If I cannot measure it, it is not an optimization. It is a preference. Preferences belong in the linter, or they belong in the trash.

I also allow a small cache when the work is obviously repeated and obviously not free: a formatted date in a row, a derived option list that does not change with scroll. I do not cache a one-line getter because a blog mentioned hidden classes in 2015. V8 has had hobbies since then. So have I.

What I ignore on purpose

I ignore micro-benchmarks of for versus map unless the loop is the trace. I ignore “avoid closures” as a house rule. I ignore object-shape lectures on a form that submits twice a day. I ignore rewriting async/await into a raw promise chain to “help the optimizer.” I ignore most memoization that was added because a render count went up in a DevTools badge and not because a frame dropped.

I also ignore the urge to hoist everything into a module-level cache. Caches have invalidation, and invalidation has bugs, and those bugs show up as “the UI is fast and wrong.” Wrong is a worse performance problem than a millisecond. Users will wait a beat. They will not wait for you to explain why the old price is still on the button.

Bundle size is a different conversation from function style. I will spend a day cutting a dependency. I will not spend a day turning array methods into indexes to save a few bytes of source that gzip will sit on anyway. If the cost is the download, measure the download. If the cost is the parse of a 4MB JSON, measure the parse. Do not measure the poetry of the loop.

The culture problem

Micro-optimization spreads like a house style. One clever PR becomes a standard. New people learn that “we care about performance” means “we rewrite reduce.” They stop looking at traces. They start looking at syntax. You get a codebase that is locally taut and globally heavy.

I kill that culture in review by asking for the gesture. “Which interaction got faster?” If the answer is “in theory this allocates less,” I ask for the theory to wait. We can take it when the list is windowed. Not before. I would rather a junior ship a readable filter than a senior ship a hand-rolled thing I have to debug on a Friday.

Agents make this worse. They will eagerly rewrite a map into a for-loop and call it a win. They have read the same posts. I put “do not micro-optimize without a trace” in the repo note I give them. They ignore it sometimes. I revert those diffs. The revert is the training.

Handwritten notes and a coffee cup on a wooden desk

A week of work that is actually speed

If I had three days on a janky product, I would not open the utils folder. I would pick the top three user paths from analytics — usually search, the main list, and save — and I would make those paths boringly fast. Reserved space. Smaller payloads. Fewer mounts. One network round-trip instead of four. A disable on the double-submit. Then I would put a performance budget in CI for the payload of those routes, not for the cyclomatic complexity of a helper.

Budgets that work are crude: this route’s JS and JSON stay under N kilobytes; this list does not render more than M row components at a time; this click handler may not read layout. Crude is enforceable. “Write fast JS” is not.

I would also delete work. A dependency that pulls a date library for one format. A session replay that runs on the checkout path. A polyfill we do not need. Deletion is the optimization that does not go stale when V8 ships a new year. INP and FPS are both feelings; I will take a slightly lower frame rate on a decorative animation if the tap on Save stays under a beat. Users forgive motion. They do not forgive a button that lies about being done.

A note on “fast enough”

Not every screen needs to feel like a game. An admin CSV export can take a second if the button says so. A marketing page can be a little heavy if it is not the product. I save the urgency for the paths that earn money or trust: search, compose, checkout, the list people live in all day. Spreading urgency across the whole app is how you get micro-optimizations in a footer and a hitch in the inbox.

Fast enough is a product sentence. Write it down. “Search results paint in one second on a mid-range Android with cache cold.” That sentence tells you whether to window the list or to rewrite a reducer. Without it, every function is a candidate and none of them are a priority.

What I tell people who want a checklist

Measure the gesture. Fix layout, lists, and waterfalls first. Keep cold paths readable. Only tighten a loop when the trace names it and a metric moves. Write the comment for the next person, including the agent. Do not let “performance” become a dialect of the language that only one engineer enjoys.

Staying fast is a habit of attention, not a habit of syntax. The functions can stay ordinary. The frames cannot. If you remember only one thing, remember that the user does not execute your helper. They execute a scroll, a tap, a type. Write for that. Let the rest of the file look like JavaScript you will still want to edit in 2027.

More articles for you