Home / Blog / JavaScript Development /JavaScript Performance Optimization: Common Bottlenecks and Best Practices

JavaScript Development

September 25, 2026 - by Devico Team

JavaScript Performance Optimization: Common Bottlenecks and Best Practices

Most JavaScript performance problems are not caused by teams having too few optimization techniques at their disposal. Quite the opposite. Developers already know about code splitting, tree shaking, debouncing, Web Workers, memoization, lazy loading, and half a dozen other possible fixes.

The difficult part is knowing which one matters now.

A slow interaction caused by a 500 ms main-thread task will not improve because somebody removed 40 KB from a bundle. A memory leak in a dashboard that stays open for eight hours has little to do with its Lighthouse score. And moving computation into a Web Worker is unnecessary complexity if the interaction is actually spending most of its time waiting for layout.

That distinction matters more as JavaScript costs continue to rise on real devices. The median mobile page now records 1,916 milliseconds of Total Blocking Time, up 58% year over year. Only 48% of mobile origins pass all three Core Web Vitals.

So the useful question is not, “How can we optimize JavaScript?” It is, “What is blocking this page or interaction, and what evidence would prove that we fixed it?”

That is the approach here. We will work from symptoms to causes, distinguish loading problems from interaction problems, look at framework-specific failure modes in React, Vue, and Angular, and finish with a way to rank performance work when there is not enough engineering capacity to tackle everything at once.

Why JavaScript costs more than its share of the page

Images still account for more transferred bytes on a typical page. JavaScript usually costs more to process.

Those are different things.

An image is downloaded, decoded, and painted. JavaScript has to be downloaded, parsed, compiled, and then executed. Much of that work happens on the main thread and depends heavily on the CPU in the user's device.

That is why faster networking does not automatically produce a responsive application. A 5G connection may deliver a bundle quickly, while the phone still spends seconds turning it into executable code and running it.

The 2025 Web Almanac puts the difference in perspective. The median desktop home page transfers about 1,059 KB of images and 697 KB of compressed JavaScript. But compression only reduces network transfer. The browser has to work with the decompressed code.

The Almanac uses amazon.co.uk as an example: 423 KB of downloaded JavaScript expands to 1,721 KB after decompression.

That work becomes much more expensive on slower CPUs. Median Total Blocking Time is 92 ms on desktop and 1,916 ms on mobile. At the 90th percentile, mobile users encounter more than 7.5 seconds of blocking time.

Same application. Same JavaScript. Very different cost.

What the JavaScript engine is actually doing

Modern engines such as V8, SpiderMonkey, and JavaScriptCore do considerably more than interpret source code from top to bottom.

A simplified execution path looks like this: code is parsed into an abstract syntax tree, passed through a fast baseline interpreter, observed while it runs, and — for functions that execute often enough — sent to an optimizing JIT compiler. The compiler can then produce machine code specialized around the inputs and object shapes it has observed.

For performance work, two consequences are more useful than the implementation details.

First, JIT optimization rewards consistency. A frequently called function that sees predictable object shapes is easier to optimize. A hot function whose objects repeatedly change structure can be deoptimized back onto slower paths.

Second, startup code does not receive the same benefit. Code that runs once during initialization may never become “hot” enough for advanced optimization at all.

This separates two kinds of work that are often mixed together. Bundle reduction and long-task reduction mostly attack startup and cold execution. Micro-optimizing a loop matters only after you know that loop is genuinely a hot path.

It also explains why arguments about for versus map(), filter(), or reduce() are usually misplaced. Current engines have made the difference insignificant for ordinary collection sizes. A single forced layout recalculation can cost more than thousands of iterations of whichever array method lost the benchmark.

At hundreds of thousands of iterations in a CPU-heavy hot loop, syntax differences can become measurable. Even then, moving the computation away from the main thread is often a more useful intervention than rewriting map() as for.

The practical order is therefore simple: reduce unnecessary execution first. Tune individual operations later.

Diagnose the bottleneck before choosing the fix

A JavaScript performance bottleneck exists whenever script-related work prevents the browser from completing something the user is waiting for: painting content, processing input, or producing the next frame.

Those failures can look similar from the outside. Their causes are not.

A page that feels unresponsive might be blocked by JavaScript execution. It might also be waiting for layout. Or a third-party tag may already own the main thread by the time the user clicks.

The diagnostic method determines which of those explanations you can see.

Lab data predicts. Field data tells you what happened.

Lighthouse and the Chrome DevTools Performance panel run under controlled or simulated conditions. That makes them useful for reproduction. You can throttle the CPU, repeat an interaction, inspect flame charts, and test a fix minutes later.

CrUX and the Core Web Vitals report in Search Console answer a different question: what happened to actual users, on their hardware and connections, over time?

Field data is slower and messier. It is also the evidence that matters.

Teams get into trouble when they treat the two as substitutes. A Lighthouse score of 96 does not prove that an application responds quickly on a four-year-old Android phone running several background processes. Field data can prove that users have an INP problem, but it cannot tell you which component or script caused it.

A better workflow is circular:

field problem → lab reproduction → code change → field confirmation

Fotocasa ran into the limitations of field aggregation directly. Search Console's 28-day aggregation told the team that interactions were performing poorly, but not which interactions were responsible. They added real-user monitoring using the web-vitals library and Datadog so they could investigate INP attribution much sooner.

Which performance tool answers which question?

Tool
Data
Best used for
Main limitation

Chrome DevTools Performance panel

Lab

Flame charts, long tasks, layout work, style recalculation, INP sub-parts

Represents one machine unless you deliberately throttle it

Chrome DevTools Memory panel

Lab

Heap snapshots, detached DOM nodes, retained objects

Most useful once you already know how to reproduce the suspected leak

Lighthouse / PageSpeed Insights

Lab, with CrUX field summaries

Quick triage, TBT, unused JavaScript, loading diagnostics

Simulated conditions; overall scores can move for unrelated reasons

CrUX / Search Console CWV

Field

Establishing whether real users are affected and at what scale

Rolling aggregation and limited attribution

Long Tasks API / PerformanceObserver

Field

Continuous visibility into main-thread blocking

Does not identify the responsible script origin by itself

Long Animation Frames (LoAF)

Field

Attributing slow frames to scripts, including third parties

Chromium-based browser support

WebPageTest

Lab

Real-device tests, filmstrips, connection profiles

Slower than DevTools for quick iteration

The less obvious gap is often attribution in production. Taboola found that the Long Tasks API showed that blocking had happened but did not reveal which script caused it. The team joined the Long Animation Frames origin trial for that reason and later reported INP improvements of up to 36% on partner sites.

Reading a flame chart without analysing every bar

Start by recording the interaction that is slow. Do not automatically profile initial page load if users are complaining about opening a menu or changing a filter.

CPU throttling matters here. Fotocasa found an interaction that looked acceptable without throttling but reached 832 ms under 6× CPU slowdown.

Once the trace is recorded, a useful first pass is surprisingly small:

  1. Find the widest main-thread tasks. Width represents duration. Anything above 50 ms qualifies as a long task. One 400 ms task deserves attention before dozens of 8–10 ms ones.

  2. Identify the entry point. The top of the stack normally points to an event handler, timer callback, or script evaluation that started the work.

  3. Watch for layout mixed into scripting. Repeating yellow scripting bands and purple layout/style work usually suggest forced layout or layout thrashing.

  4. Read the Interactions track. INP can be separated into input delay, processing duration, and presentation delay.

That last split prevents a lot of wasted work.

If processing duration is large, the event handler or the work it starts may indeed be expensive.

If input delay dominates, the interaction waited because some earlier task already occupied the main thread. Optimizing the click handler will barely matter.

If presentation delay is large, the browser is struggling to render the result after the handler runs. Look at layout, style recalculation, rendering volume, or framework re-renders.

Check that JavaScript is actually the problem

There is little value in spending a sprint on JavaScript if the page is slow for another reason.

A bad Time to First Byte points toward the server, cache, or CDN.

A poor LCP can come from an oversized hero image. Images are the LCP element on 76% of mobile pages.

CLS often has a simpler explanation: missing dimensions and unreserved space. At least one image lacks explicit dimensions on 62% of mobile pages.

Lighthouse can surface those issues quickly. Rule them out before reaching for a JavaScript-specific solution.

How JavaScript affects LCP, INP, CLS, and TBT

FID is no longer the interaction metric

Interaction to Next Paint replaced First Input Delay as a Core Web Vital on March 12, 2024.

FID measured how long the browser waited before beginning to process the first interaction. That left large parts of the experience unmeasured.

INP observes interactions across the visit and measures the delay from input until the browser can paint the resulting frame. The move exposed problems that FID could not.

Fotocasa is a good illustration. Its pages were largely inside the FID “Good” threshold. After the switch to INP, many landed in “Needs improvement” or “Poor” even though the application had not suddenly become slower. The metric had simply started measuring more of the waiting time users already experienced.

Where JavaScript enters each Core Web Vital

Metric
What it measures
Good threshold at p75
Typical JavaScript contribution

Time until the largest visible element renders

≤ 2.5 s

Render-blocking scripts, client rendering that delays primary content, hydration before the hero can paint

Interaction latency across the visit

≤ 200 ms

Main-thread long tasks, expensive handlers, re-render cascades, synchronous third-party execution

Unexpected layout movement

≤ 0.1

Late-injected banners, dialogs, ads, and hydrated widgets without reserved space

Lab measurement of blocking after FCP

≤ 200 ms

Sum of the blocking portions of long tasks during loading

The desktop/mobile split is useful context. Around 97% of desktop origins achieve good INP, compared with 77% on mobile. LCP passes at roughly 74% on desktop and 62% on mobile.

Across the top 1,000 sites, good INP falls to 63%. Complex, highly interactive products are exactly where interaction latency becomes harder to control.

Another split is easy to miss. Mobile home pages reach roughly 80% good INP, while secondary pages are closer to 69%.

That makes intuitive sense. Home pages get audited repeatedly. Product filters, account settings, listing screens, reporting views, and internal dashboards receive less attention even though users perform far more interactions there.

The connection between performance and business results needs careful wording

Performance case studies often get reduced to impressive conversion statistics without the qualifications that made the original result credible.

Two published examples are more useful.

Fotocasa reported that its performance work, together with other initiatives, contributed to a 27% increase in contact and phone lead ads. The qualification matters: performance was not presented as the only cause.

Trendyol reported a 50% INP reduction on its product listing page and a 1% improvement in a search-results business metric. On a large ecommerce platform, one percentage point can represent a substantial absolute volume.

That still does not justify promising that an INP project will increase conversion by a fixed percentage.

The stronger internal case is measurable without making that leap: a known share of users is experiencing latency beyond an accepted threshold, the problem can be attributed to specific interactions or page groups, and the work can be scoped against that metric.

Bottleneck 1: Long tasks and main-thread blocking

JavaScript runs to completion within a task. If a task occupies the main thread for 300 ms, the browser cannot interrupt it halfway through to handle a click. The click waits.

Once a task passes 50 ms, it is classified as a long task. For interactive applications, these tasks are one of the most direct routes to poor INP.

You will usually notice them through behaviour before the trace tells you the cause: delayed clicks, typing that lags behind the keyboard, menus that appear a fraction of a second late, or an interaction that feels inconsistent across devices.

Common sources include:

  • parsing or transforming a large JSON payload synchronously;

  • sorting or filtering thousands of records on the main thread;

  • formatting a large data table;

  • client-side search over a substantial dataset;

  • a framework state update that causes far more components to render than expected;

  • hydration across a large server-rendered tree;

  • third-party scripts executing during the period when users begin interacting.

The first response should not be Web Workers. Start with the cheapest question: does all of this work need to happen at this moment?

Delete or delay before you optimize

Analytics, session recording, experimentation frameworks, and non-critical instrumentation rarely deserve CPU time ahead of the user's first meaningful interaction.

Fotocasa improved its interaction path partly by postponing analytics work while the browser was processing user-facing updates. Nothing about the analytics algorithm became faster. It simply stopped competing for the main thread at the wrong time.

If the work itself is required, the next option is to divide it.

scheduler.yield() gives a long operation opportunities to return control to the browser so queued interactions can run between chunks. Trendyol used this approach as part of the work that produced its 50% INP reduction.

Browser support still requires care. MDN currently marks scheduler.yield() as not Baseline across all major browsers, so feature detection and fallbacks such as setTimeout() or scheduler.postTask() remain relevant.

Yielding is not parallelism. scheduler.yield() and postTask() still execute JavaScript on the main thread; they change scheduling.

For genuinely CPU-heavy computation — image processing, cryptography, large transforms, substantial parsing — a Web Worker can move the work away from the UI thread entirely.

Hydration is a different class of problem again. If the browser is blocked attaching behaviour across a very large tree, task scheduling may only disguise an architectural cost. Selective hydration, islands, or server components may be more appropriate.

How to verify the fix

Profile the same interaction, on the same page, under the same CPU throttling.

The relevant long task should shrink or disappear. TBT can act as an early lab signal during loading, while field INP tells you whether users actually benefited.

The Economic Times illustrates why both levels matter. Its team reduced lab TBT from 3,260 ms to 120 ms, then observed corresponding improvements in field interaction performance.

Do not call the work finished simply because the Lighthouse score increased.

And check the INP sub-parts before doing any of this. A large presentation delay is primarily a rendering problem. Large input delay means another task blocked the thread before the interaction started. Neither will be solved by shaving milliseconds from the event handler itself.

Bottleneck 2: Oversized and poorly structured bundles

Bundle size is easy to see, which makes it easy to prioritize.

Sometimes that is correct. Sometimes it is merely satisfying.

A large JavaScript bundle affects new sessions because users have to transfer, parse, compile, and potentially execute the code before the application becomes useful. It matters most on mobile and on routes where critical content depends on client-side rendering.

It matters much less when the complaint is, “The dashboard becomes sluggish after I've been using it for half an hour.”

The 2024 Web Almanac found that a median mobile page shipped 206 KB of unused JavaScript against roughly 558 KB total. A substantial portion of what browsers download and process is never called on that page.

There are only a few recurring reasons:

  • a large package was imported to use a small part of its API;

  • tree shaking is configured in theory but ineffective in the final build;

  • all routes are bundled together;

  • code needed later is being loaded now.

Start with the bundle map

Run a tool such as webpack-bundle-analyzer, rollup-plugin-visualizer, or the equivalent analysis tooling in your build system.

Do that before deciding that “we need more code splitting.”

A treemap often reveals one disproportionately expensive dependency: an entire icon library for a handful of icons, a date library for one formatting operation, or a charting package included in the initial bundle even though charts only appear behind one tab.

Replacing that package may be a one-day change with a larger effect than a week of lower-level tuning.

Tree shaking can be enabled and still accomplish very little

Tree shaking depends on statically analyzable ES modules.

CommonJS packages are difficult or impossible for a bundler to shake effectively. Modules with side effects may have to be retained. Namespace imports and barrel re-exports can also pull in considerably more code than developers expect.

A "sideEffects": false declaration helps only when it is accurate.

The analyzer output is the truth. A green check mark in the bundler configuration is not.

Code splitting works best at meaningful boundaries

Route-level splitting is usually the clearest win: people should not download every screen in the application to open one route.

Component-level lazy loading makes sense for unusually heavy interface elements that may never be shown in the session — a rich-text editor, map, advanced chart, large data grid, or similar optional feature.

Splitting everything creates another problem.

Each chunk still has to be requested. If a child chunk is only discovered after its parent downloads and executes, the browser creates a request waterfall:

download parent → execute parent → discover child → request child → wait

Ten sequential small files can lose to one moderate file.

A reasonable rule is to split where the next user action is genuinely uncertain. Where the next request is predictable, preload or prefetch rather than forcing the browser to discover it late.

Compression helps transfer, not execution

Minification remains useful because it reduces both transfer size and some parser work.

Brotli generally produces smaller text assets than gzip and is broadly supported. It should be enabled for JavaScript, CSS, and other compressible resources.

But Brotli does not make JavaScript cheaper after decompression. HTTP/2 multiplexing does not reduce parse or execution time either.

Those optimizations address the network side of the problem. Do not credit them with CPU savings they do not produce.

Change
Typical effort
Main payoff

Replace one oversized dependency

Low

Removes transfer bytes plus parse/compile cost

Route-level code splitting

Low–medium

Defers much of the application until a route is requested

Repair ineffective tree shaking

Medium

Eliminates unused exports without changing behaviour

Lazy-load a heavy conditional component

Medium

Keeps optional UI out of the initial path; can create waterfalls if overused

Brotli compression

Low

Reduces transfer bytes; does not reduce execution work

After shipping the change, compare the bundle analysis and then look at the field metric the change was meant to influence.

If initial JavaScript drops by 30% and LCP does not move, the reduction may still be worthwhile for maintainability and transfer cost. It just was not a meaningful fix for that LCP problem.

That distinction is useful information for the next prioritization decision.

Bottleneck 3: DOM and rendering work

Saying “DOM operations are slow” hides the actual problem.

Changing the DOM is often cheap. Forcing the browser to recalculate layout repeatedly in the middle of JavaScript is not.

Reflow and repaint are different costs

Reflow / layout
Repaint

Browser work

Recalculates element geometry, potentially across much of the page

Redraws pixels without recomputing geometry

Common triggers

Width, height, margin, padding, position, font size, inserting/removing elements, layout reads after writes

Color, background, visibility, box shadow

Relative cost

Can become expensive as the affected tree grows

Usually cheaper

Animation preference

Avoid repeated geometric changes when possible

Prefer transform and opacity, which can often stay on the compositor

The classic failure pattern is layout thrashing.

Suppose code changes the DOM and then immediately reads offsetWidth, getBoundingClientRect(), or scrollTop. The browser has pending layout work, but it must flush that work immediately to return an accurate value.

Now put the write-read sequence inside a loop.

Instead of calculating layout once after the updates, the browser may recalculate it again and again.

You do not need hundreds of forced recalculations for the effect to become measurable. Fotocasa traced one high-INP interaction to two style recalculations triggered by changes to document.body.

The fixes are usually boring

Read layout information first. Write afterward.

If several DOM nodes need to be constructed, build the subtree away from the live document — for example with DocumentFragment — and insert it once.

For large collections of similar elements, event delegation can replace hundreds or thousands of individual listeners with one listener on the container.

Long lists need a different answer. Rendering 10,000 rows and then trying to optimize the DOM operations is solving the wrong problem. Virtualize the list so the browser only maintains what is visible or close to visible.

Animation choices also matter. Non-composited animations still appear on roughly 40% of mobile pages. transform and opacity generally avoid the expensive geometry path used by properties such as width or top.

For CLS, reserve space for anything that will arrive after the initial paint: banners, ads, dialogs, embedded widgets, images, and asynchronously loaded components.

Measure presentation delay, not only handler duration

After a rendering fix, style-recalculation and layout blocks in the Performance panel should become smaller or disappear.

Pay particular attention to presentation delay in the INP breakdown.

If your handler completes in 30 ms and the browser then needs another 250 ms to produce the frame, the handler is not the part that deserves a rewrite.

Frameworks complicate the picture slightly. React and Vue already batch many updates, so hand-written layout thrashing is often less common than it was in imperative applications. It tends to enter through charting libraries, third-party widgets, measurement-heavy effects, or custom DOM code.

In a framework application where rendering is expensive, component volume is often a better place to look than raw DOM API speed.

Bottleneck 4: Memory leaks in long-running applications

Garbage collection can only reclaim objects that are no longer reachable.

A JavaScript memory leak therefore usually means the collector is behaving correctly: something in the application still holds a reference to data that should have been released.

That gives memory leaks a useful property. Once reproduced, they are traceable.

Common causes include detached DOM nodes still referenced from JavaScript, timers that survive a component, listeners attached to window or document without cleanup, closures that retain large objects, module-level caches with no upper bound, and observers or WebSocket connections that are never disconnected.

The symptom is often temporal.

A dashboard is responsive after login and unpleasant by the afternoon. An SPA gets progressively slower after repeated route changes. Opening and closing a complex dialog twenty times consumes more memory each time. Reloading the page makes the problem disappear.

That pattern is more informative than a high memory number in isolation.

Reproduce the leak as a reversible operation

Open Chrome DevTools and take a baseline heap snapshot from the Memory panel.

Then repeat something that should return the application to its original state:

  • open and close the same modal several times;

  • visit a route and navigate back;

  • mount and unmount a component;

  • connect and disconnect a feature.

Take another snapshot and use the Comparison view.

Sort by delta. Search for detached nodes if DOM retention is suspected. On an object that should have disappeared, inspect the retainers path.

That chain is usually more valuable than the total heap size because it shows what still references the object.

Chrome's official memory guidance describes the same principle: a leak is demonstrated by objects that survive after they should have become unreachable.

Normal memory use and leaking memory look different over repeated cycles.

Healthy usage often climbs during work, then settles or drops after garbage collection.

A leak ratchets upward. Run the same reversible action five times and some objects survive every cycle.

Cleanup should live next to setup

Framework lifecycle APIs are the obvious place:

  • a return function from useEffect in React;

  • onUnmounted in Vue;

  • ngOnDestroy or DestroyRef in Angular.

AbortController can make cleanup less fragile. Give a component one controller, pass its signal into supported listeners and requests, then call abort() during teardown rather than manually remembering each resource.

Caches should have a bound. In a long-running SPA, an unbounded Map is not automatically harmless because its entries are called “cache.”

For metadata associated with objects or DOM nodes, WeakMap — and in narrower cases WeakRef — can allow entries to disappear when the owning object is collected.

Verification is unusually binary here. Repeat the same snapshot comparison after the fix. If the retained object count no longer grows, the leak is gone.

You do not need 28 days of field data to establish that.

Priority still depends on product usage. A leak on a marketing page that users keep open for ninety seconds is rarely urgent. The same leak in an IDE, support console, trading application, or operations dashboard can become the highest-priority performance issue in the system.

Bottleneck 5: Third-party JavaScript

Third-party code has an organizational advantage over application code: engineering may not even know it exists.

Tags can be added through consent platforms or tag managers by marketing, analytics, advertising, growth, and customer-support teams. They never appear in your Git repository and may not appear in the application's bundle analyzer.

They still run on the same main thread.

Analytics libraries, A/B testing tools, session recording, advertising scripts, chat widgets, personalization software, and consent platforms all compete with your own code for CPU.

async does not mean “free.” An asynchronous script may stop blocking HTML parsing, but once it executes it can still consume hundreds of milliseconds at exactly the time a user wants to interact.

Taboola documented third-party execution occupying the main thread for 691 milliseconds in a single task.

At that point, optimizing a 12 ms application function is irrelevant.

Start with an inventory of what actually loads in production. Not what the architecture diagram says loads. Open the live page and identify scripts by origin.

Old experiments and abandoned tools have a habit of surviving.

Then ask whether each script must run before the page becomes interactive. Many do not.

Delay non-critical execution. Use async and defer according to the dependency rather than as interchangeable checkboxes. Where appropriate, investigate worker-based isolation.

For long-term control, give third-party JavaScript a budget just like application code: transferred weight, execution time, and an accountable owner.

LoAF can attribute main-thread time by origin. Another useful test is to run the same page in WebPageTest with selected third parties blocked and compare TBT.

The resulting number is often more effective than an abstract argument with the team that owns the tag.

Third-party performance is partly an engineering problem. Quite often it is a governance problem disguised as one.

React, Vue, and Angular fail in different ways

The browser-level bottlenecks are shared. Frameworks influence how teams create them.

Framework
Default model
Frequent performance failure
Typical response

React

State/context changes cause component renders and reconciliation

Cascading re-renders from state placed too high or unstable references

Colocate state, use stable keys and references, consider React Compiler

Vue

Fine-grained proxy-based reactivity

Deep reactivity applied to large or externally owned data

shallowRef(), markRaw()

Angular

Current versions support zoneless, signal-driven updates

Older zone.js applications run change detection more often than necessary

OnPush, signals, zoneless migration

React: the expensive part may be where the state lives

React's performance behaviour is easier to reason about when you stop treating every re-render as a problem.

A state update renders the component that owns the state and normally causes its descendants to be considered for rendering as well. That becomes expensive when state sits far above the small piece of UI that actually needs it.

Fotocasa published a particularly clear example.

A filter-dialog state value lived in the Search page. Opening the dialog therefore re-rendered the whole page. Under 4× CPU throttling the interaction reached 440 ms; at 6× it measured 832 ms.

They moved the state into the button component that actually owned the interaction.

The same action dropped to 64 ms at 4× slowdown and 232 ms at 6×.

No memoization layer. No worker. No new library. The application simply stopped rendering components that had nothing to do with the state change.

Another Fotocasa issue came from duplicated state. A filter count that could be calculated from existing values was stored separately in useState and synchronized with useEffect.

That created an additional render. Because the page was server rendered, it could also contribute to a layout shift.

Replacing the state/effect pair with a derived value removed both behaviours.

Derived values stored as state are easy to dismiss as code style. In a large React tree, they can become a performance problem.

React Compiler changes memoization, not architecture

React Compiler reached its first stable release on October 7, 2025.

It can introduce memoization during compilation at a level that is difficult to reproduce manually. React reported up to 12% improvements in initial loads and cross-page navigation in Meta Quest Store, with some interactions more than 2.5× faster and no increase in memory usage.

That does not mean useMemo and useCallback should now be deleted mechanically.

React's own guidance keeps manual memoization as an escape hatch where developers need explicit control, including cases where a memoized value is important to an effect dependency. Existing memoization should be removed only after testing because doing so may change compiler output.

More importantly, the compiler addresses unnecessary work caused by references and rendering.

It cannot repair state architecture. It cannot make genuinely expensive computation cheap. It cannot eliminate a network waterfall.

For applications dominated by unnecessary render volume, it can help substantially. In codebases that violate the Rules of React, some components may simply be skipped because the compiler cannot prove that transformation is safe.

Vue: not every object needs deep reactivity

Vue's deep reactivity is convenient for ordinary application state. It is unnecessary work for data that never needs to participate in reactive updates.

Wrap a large object in ref() or reactive(), and nested values become part of the proxy-based reactivity system.

On a 20,000-row immutable dataset, third-party chart instance, map object, or WebGL scene, that can be a considerable amount of machinery with no UI benefit.

Vue explicitly documents shallowRef() for large data structures and external state integrations. markRaw() excludes a value from proxy conversion entirely.

The trade-off is control.

With a shallow reference, deep mutations do not automatically trigger reactive updates; you may need triggerRef() or to replace the root reference. Raw objects can also introduce identity edge cases when raw and proxied versions appear in the same graph.

Use those APIs where deep tracking is objectively wasted work, not as a default replacement for Vue's reactivity model.

Angular: performance advice depends heavily on the version

For years, Angular applications commonly relied on zone.js, which patches asynchronous browser APIs so Angular knows when something may have changed.

The model made state updates convenient but could trigger change detection more often than the application actually needed.

Angular has moved away from that default.

Zoneless change detection became stable in v20.2, and Angular announced that zone.js is no longer included by default from v21 onward. New applications can use signals and zoneless updates without carrying the historical runtime behaviour.

For established applications, migration is not a one-line performance setting.

OnPush remains useful. Signals can reduce broad change detection. runOutsideAngular() can keep appropriate work from unnecessarily triggering the framework. trackBy remains particularly valuable for large lists.

Zoneless migration is often the destination rather than the first ticket, because it exposes every place where mature code implicitly depended on zone-driven updates.

That cost can be worthwhile in a large enterprise application where change detection scales with a substantial component tree. In a small Angular product whose traces show no meaningful framework overhead, migration should not outrank more visible bottlenecks.

What about Svelte?

Svelte shifts much of the framework work to compilation rather than shipping a large runtime reactivity system.

That lowers one category of overhead. It does not grant immunity from browser performance rules.

A Svelte application can still run a 400 ms handler, load a collection of expensive third-party tags, force repeated layout, or render more data than the browser can handle.

Framework choice influences baseline overhead. It does not determine whether an application is fast.

Which JavaScript bottleneck should you fix first?

A team with a full roadmap rarely has spare capacity to “optimize performance” as a broad initiative.

A useful priority order has to consider more than theoretical impact.

Bottleneck
Detection difficulty
Typical effort
Main CWV effect
Cost of ignoring it
Default priority

Long tasks / main-thread blocking

Low

Medium

High — INP, TBT

Tends to worsen as features accumulate

1

Third-party scripts

Technically low, organizationally harder

Low technically

High — INP, LCP

Can grow without engineering review

2

Framework render cascades

Medium

Medium

High — INP

Compounds as the UI grows

3

Oversized bundles

Very low

Low–medium

Medium — LCP, FCP

Gradual first-load degradation

4

DOM / rendering cost

Medium

Often low once found

Medium — INP presentation delay, CLS

Usually concentrated on particular screens

5

Memory leaks

High

Low–medium after diagnosis

Not directly represented by CWV

Can be severe in long sessions

Conditional

This is deliberately not a universal ranking.

Long tasks make a sensible default starting point for interactive products because a trace exposes them quickly and early interventions such as deleting or deferring work do not require architectural change.

Third-party code deserves attention early because the technical fix may be easy while the organizational fix takes weeks.

Framework re-renders rise in priority in mature applications where a small state change touches a large interface.

Bundle size sits lower than many optimization checklists would put it. That is intentional.

For a public ecommerce site with large amounts of new-session traffic, bundle work may move straight to the top. For an internal application that users load once and keep open for six hours, responsiveness matters much more than trimming another 80 KB from initial transfer.

Memory is entirely usage-dependent. A leak that is harmless during a short checkout session can cripple an application designed to stay open throughout a workday.

Three rules prevent a lot of low-value optimization

Start from field evidence. A Lighthouse suggestion estimating a 300 ms saving does not prove that meaningful numbers of users have the problem.

Remove work before making work faster. Deleting a tag, deferring an analytics callback, colocating React state, or avoiding a render can be both safer and more effective than introducing a worker, cache, or additional memoization.

Stop when further gains stop mattering. Performance engineering has a maintenance cost. Moving INP from 600 ms to 180 ms is a different kind of win from moving it from 180 ms to 140 ms.

This is also a useful standard for an external audit. Work delivered by internal engineers or by JavaScript software development companies should result in a ranked set of problems tied to observable metrics. A spreadsheet containing forty unordered Lighthouse recommendations is not a performance strategy.

Preventing performance regressions

A successful optimization that is not defended tends to disappear gradually.

Usually nobody deliberately reintroduces the problem.

A pull request adds a dependency. Another introduces an experiment tag. A third changes a route to client rendering. Each change looks reasonable in isolation, and the cost is almost invisible in code review.

A performance budget has to be enforceable

Useful budgets are few enough for developers to understand:

  • maximum initial-route JavaScript size;

  • a TBT ceiling on a small set of high-traffic journeys;

  • a cap on third-party script size or execution time.

Lighthouse CI and bundle-size checks in GitHub Actions can enforce these limits automatically.

Whether the check blocks the merge matters more than the particular product chosen. A warning that everybody learns to ignore is not much of a budget.

Lab checks catch code changes. RUM catches reality.

CI can stop obvious regressions before deployment.

It cannot tell you how the release behaves across the distribution of real devices your customers use.

The web-vitals library can collect LCP, INP, and CLS together with attribution information and send it to the telemetry system already used by the team.

Fotocasa went further and routed Core Web Vitals anomalies into Slack. Treating a sudden INP regression in the same operational category as another production signal is much more likely to preserve improvements than checking Search Console every few months.

Performance debt also needs an owner.

That does not require creating a new department. Somebody should know which metric they are responsible for, field performance should appear in the same engineering review cadence as error rates, and pull requests that introduce dependencies or new third-party scripts should make the expected cost visible.

The process is unglamorous. That is partly why it works.

When performance debt becomes a capacity problem

Most performance bugs belong in the backlog.

A slow filter, oversized package, leaking modal, or blocking callback can be reproduced, fixed, verified, and closed.

A different pattern appears when performance tickets continue shipping but the underlying metric stays poor.

Warning signs include:

  • the same Core Web Vital has remained outside the target for several months despite local fixes;

  • no team or engineer has clear ownership of performance;

  • the diagnosis depends on skills the team does not currently use — such as heap-retainer analysis or detailed INP attribution;

  • the real fix is a migration rather than a local code change;

  • feature delivery has already slowed because performance work is consuming capacity informally.

Repeated local symptoms can point to a structural cause: hydration strategy, rendering architecture, state placement, framework migration, or a third-party governance problem.

At that point, adding another isolated ticket is unlikely to change much.

One option is protected internal capacity: an engineer or small squad whose primary responsibility for a defined period is the performance initiative.

Another is temporary specialist support. Under delivery pressure, teams sometimes hire experienced JavaScript developers specifically to diagnose and execute a time-boxed performance program alongside the core roadmap.

That model works best when the scope is measurable — reduce INP for a defined set of journeys, remove a known source of main-thread blocking, migrate a rendering path — rather than “make the app faster.”

External help also comes with two limits that should be explicit.

Nobody can promise a precise field Core Web Vitals result solely from code changes. Real-user metrics depend partly on hardware, network conditions, traffic mix, and third-party code outside the engineering team's control.

And if the specialists leave while nobody inside the company can read the traces, operate the monitoring, or understand the budgets, the organization has transferred the performance problem rather than solved it.

Knowledge transfer and regression protection matter as much as the one-time speedup.

Where to start this week

Do not begin by opening an optimization checklist.

Open your field data.

Find the Core Web Vital that is actually failing and narrow the problem to a page group, route, or interaction wherever possible.

Then reproduce the worst case under CPU throttling and inspect the INP breakdown. Determine whether time is being lost before the handler, inside the handler, or while the browser tries to render the result.

Spend an hour inventorying third-party JavaScript. That exercise is cheap and occasionally changes the entire plan.

Only then choose the first bottleneck.

Fix one thing. Measure it under the same lab conditions. Confirm that the expected field metric moves. After that, take the second item from the list.

The less disciplined alternative is familiar: shrink the bundle by 30%, improve a Lighthouse score, ship several clever optimizations — and discover that users still have exactly the same 500 ms interaction.

JavaScript performance work becomes much more predictable once the diagnosis determines the technique instead of the other way around.

Frequently Asked Questions

What is a JavaScript performance bottleneck?

A JavaScript performance bottleneck is work related to scripts that prevents the browser from promptly rendering content, responding to input, or producing the next frame. Common categories include main-thread long tasks, excessive JavaScript on initial load, unnecessary rendering, memory leaks, and third-party scripts. The useful distinction is that each category requires different evidence and a different fix.

What replaced First Input Delay as a Core Web Vital?

Interaction to Next Paint replaced First Input Delay on March 12, 2024. FID measured the delay before processing began for the first interaction. INP considers interactions throughout the visit and measures latency through the next rendered frame. Google's “Good” threshold is 200 ms or less at the 75th percentile.

Does code splitting always make a site faster?

No. Route-level splitting is generally effective because it avoids loading screens a visitor has not requested. Excessive splitting can create request waterfalls when one chunk must execute before another chunk is discovered. Split at meaningful boundaries and preload predictable next-step resources.

How can I tell a memory leak from normal memory growth?

Repeat an action that should restore the application to its previous state, such as opening and closing a modal or navigating to a route and back. Compare heap snapshots before and after using Chrome's Comparison view.

Healthy memory usage may rise and later fall after garbage collection. A leak repeatedly retains additional objects after each cycle and does not return toward the previous baseline.

Which JavaScript framework has the best default rendering performance?

Frameworks that move more work to compilation, such as Svelte, can ship less runtime overhead. Vue's fine-grained reactivity can also avoid some broad component work that React applications may encounter.

That does not determine the performance of a production application. State architecture, third-party execution, data-fetching patterns, bundle design, DOM size, and synchronous computation can outweigh framework runtime differences.

Are for loops faster than map() and filter()?

Usually by an amount that does not justify choosing them for performance reasons.

The difference becomes relevant mainly in unusually large, CPU-heavy loops. Before changing collection syntax, verify that the loop appears as a meaningful hot path in a profile. If it occupies the main thread long enough to affect interaction latency, moving the computation into a Web Worker may produce a much larger gain.

How do I know whether a JavaScript performance optimization worked?

Verify the metric connected to the bottleneck.

For a long task, reproduce the interaction and confirm the task has disappeared or become shorter, then watch field INP.

For bundle work, compare build output and field loading metrics such as LCP.

For rendering work, inspect layout/style cost and presentation delay.

For a memory leak, repeat the heap-snapshot comparison and confirm that objects no longer accumulate.

A better Lighthouse score can be useful supporting evidence. On its own, it is not proof that the original user-facing problem is fixed.

Stay in touch

Leave your email and we will inform you about all our news and updates

 

Up next