Introduction
A web app can have a fast server, a fast database, and a clean API, and still feel slow. Most of what a user experiences happens in the browser: the JavaScript has to download, parse, and run, the page has to render, and every click has to produce a visible response quickly.
Client-side performance is not about winning a benchmark. It is about how the app feels on a real phone, on a real network. A page that loads in one second on a developer laptop can take six seconds on a mid-range phone with a weak connection — and that gap is where users leave.
This guide covers how to measure speed, shrink what the browser downloads, render efficiently, cache sensibly, and move heavy work out of JavaScript when it makes sense.
Why Client-Side Performance Matters
Speed is a feature, not a polish step. When a page is slow, users do not wait patiently — they leave, tap again, or close the tab and try a competitor. Google research has consistently shown that as page load time rises, the chance a visitor leaves rises with it, especially on mobile.
Performance also affects search ranking and cost: every extra megabyte is downloaded again and again by every visitor.
The most important point is that slowness is often invisible in development: your laptop is fast and your cache is warm, while your users are on slower devices, slower networks, and cold caches. Think of the browser as having a budget — spend it on things the user can see and feel, not on code that runs before it is needed.
Measuring Performance: Core Web Vitals
You cannot improve what you do not measure. The standard measurement is Google's Core Web Vitals, three metrics that describe how a page feels to a real user.
Largest Contentful Paint, or LCP, measures how long it takes for the largest visible part of the page to appear, usually the main heading, hero image, or main content block. A good LCP is under 2.5 seconds. If the main content takes longer, the page feels empty, even if the rest has loaded.
Interaction to Next Paint, or INP, measures how quickly the page responds to a click, tap, or key press — the time from the interaction to the next visible update. A good INP is under 200 milliseconds. If a button takes half a second to respond, the app feels laggy, even if the page loaded quickly.
Cumulative Layout Shift, or CLS, measures how much the page moves around while it loads. If text jumps down because an image loaded late, or a button moves just as the user tries to tap it, that is layout shift. A good CLS score is under 0.1.
Measure them in Chrome DevTools and Lighthouse, and check real-world data in PageSpeed Insights. Start with your slowest page, on a mid-range phone with a slow network.
Bundle Size and Code Splitting
The biggest cause of slow client-side apps is JavaScript bundle size. A bundle is all the JavaScript the browser must download, parse, and execute before the page becomes interactive. A few hundred kilobytes can be fine. Two or three megabytes, common in apps that import large libraries without thinking, will be slow on any phone.
First, find out what is in your bundle. Tools such as webpack-bundle-analyzer show which packages take the most space: a date library, chart library, or UI kit imported in full can be larger than the rest of your app. Import only what you need, or replace a large library with a smaller one.
Code splitting is the next step: instead of sending the whole app in one file, split it into chunks that load only when needed. An admin panel or chart view does not need to load when a visitor first opens the home page:
const AdminPanel = lazy(() => import("./AdminPanel"));
Also check for duplicate code and ship a minified production build, not a development build.
Lazy Loading and Images
Not everything on a page needs to load at once. Lazy loading means loading a resource only when it is about to be used. Images below the fold, videos that are not playing, and sections the user has not scrolled to can all wait. That makes the first paint faster, because the browser is not busy downloading things the user cannot see yet.
For images, the loading="lazy" attribute tells the browser to wait until an image is near the viewport:
<img src="product.jpg" alt="Product photo" loading="lazy" width="800" height="600">
Always include the width and height. Without them, the browser does not know how much space to reserve, the page shifts when the image loads, and your CLS score gets worse. For responsive images, use srcset so the browser can choose a smaller file on a small screen.
For JavaScript, lazy load the same way: a chat widget, comment form, or map can load after the main content, or only when the user clicks a button. Compress every image before you ship it: a hero image of 500 kilobytes can often fall under 100 kilobytes with no visible difference.
Rendering and the DOM
Once the JavaScript has loaded, the browser has to render the page. The Document Object Model, or DOM, is the browser's representation of the page, and every element is a node in it. When JavaScript changes the DOM, the browser recalculates the layout and repaints the screen. Small changes are cheap; large changes, repeated many times, are expensive.
A common mistake is updating the DOM inside a loop. Adding a thousand rows to a table one at a time forces the browser to recalculate the layout a thousand times; build the rows in memory first, then add them all at once. Likewise, group layout reads together, then group writes together, so the browser does the work once.
Long lists are another problem: rendering ten thousand items at once will freeze the page. The solution is virtualisation — render only the items visible on screen, plus a few above and below — and libraries for this exist for most frameworks.
Finally, keep the main thread free: if a task takes more than 50 milliseconds, the user feels the delay, so break long tasks into smaller chunks. For animations, prefer transform and opacity, which avoid layout recalculation.
Caching and Service Workers
Caching means storing a resource so the next visit does not have to download it again. A well-cached app feels instant on the second visit, because the browser already has most of what it needs. A poorly cached app downloads the same files again and again.
For static assets, use long cache times with content-based file names: a file such as app.3f9a2c.js can be cached for a year, because any change produces a new name. The HTML file itself should have a short cache time, since it tells the browser which version of each asset to load.
For data, the right cache depends on how often it changes: a product list that changes daily can be cached for an hour. The Cache-Control header sets this, with max-age, no-cache, and no-store.
A service worker runs in the background and can serve cached files when the network is slow or offline. A safe pattern is to cache the app shell — the HTML, CSS, and JavaScript for the basic layout — and fetch data from the network, falling back to the cache if it fails. Start with good HTTP caching first, and add a service worker only when you need offline support.
JavaScript Frameworks: Choose Wisely
Frameworks such as React, Vue, Angular, and Svelte help you build apps faster, but they are not free. Every framework adds JavaScript the browser must download and run before your app can start, and the cost varies a lot. Some ship a small runtime and compile most work away at build time; others ship a large runtime and do more work in the browser.
The right framework is the one that fits your app, not the most popular one. A simple marketing site or blog does not need a large single-page application framework; a mostly static page is often faster as plain HTML with a little JavaScript.
If you do use a framework, watch how it updates the page. React and Vue use a virtual DOM, comparing new state with old state and updating only what changed — but that is not free. If a component re-renders far more often than needed, the cost adds up, so use memoisation and keep state close to the component that uses it.
Whatever you choose, measure it: build a small version, check its bundle size and Core Web Vitals, and compare it with the alternative.
WebAssembly for Heavy Work
Sometimes JavaScript is not the right tool. It is fast for most web work, but not designed for heavy computation: image processing, video encoding, physics simulations, data compression, or large-scale number crunching. In those cases, WebAssembly, or Wasm, can be better.
WebAssembly is a binary format that runs in the browser alongside JavaScript. Code written in Rust, C, C++, or Go can be compiled to Wasm and run at near-native speed — not as a replacement for JavaScript, but as a way to run one specific heavy task faster.
A good example is image editing: applying a filter to a large image in JavaScript can take seconds and freeze the page, while the same filter compiled to Wasm can run in a fraction of the time, in a web worker off the main thread. Other good uses are audio processing, encryption, and parsing large files.
Wasm is not free: the module must be downloaded and compiled, and moving data between JavaScript and Wasm has a cost. Pick one heavy function, try it in Wasm, and measure before and after on a real phone. If the gain is clear, keep it; if not, stay with JavaScript.
Common Mistakes to Avoid
The first mistake is optimising before measuring. Developers often spend days rewriting code that was never slow, while the real problem — a huge image, a third-party script, or a render loop — goes unnoticed. Measure first, find the biggest bottleneck, and fix that. Then measure again.
The second mistake is adding too many third-party scripts. Analytics, chat widgets, ad scripts, and tracking pixels all add JavaScript. Audit them regularly, remove the ones you do not use, and load the rest after the main content.
The third mistake is shipping development code to production. Development builds include extra checks, source maps, and unminified code, and are much larger and slower than production builds. Always build for production, enable minification and compression, and check the output before you deploy.
The fourth mistake is ignoring the network. A page that loads in a second on a fast office connection can take ten seconds on a slow mobile network. Test with network throttling in DevTools, and test on a real phone.
Finally, do not treat performance as a one-time task. Add a performance budget to your build and check your Core Web Vitals every month, because one new library or image can undo months of optimisation.
Conclusion
Client-side JavaScript performance is not about clever tricks. It is about respecting the user's device, network, and time. Measure with Core Web Vitals, shrink the bundle, load only what is needed, render efficiently, cache sensibly, and move heavy work to the right place — the server, a worker, or WebAssembly.
Start with the biggest win: measure your slowest page, fix the largest bundle or slowest interaction first, then measure again and repeat. Most apps do not need a rewrite — just a few clear changes, checked against real numbers. Do that, and your app will feel fast, which is what your users actually notice.
Frequently Asked Questions
What is the fastest way to make my web app feel faster? Reduce the JavaScript that loads on the first screen. Split your bundle, lazy load what is not needed yet, and remove unused libraries and third-party scripts. In most apps, that one change improves loading time more than any other.
Do Core Web Vitals really matter for my app? Yes, because they measure what users actually feel: how fast the main content appears, how quickly the page responds, and how much the layout jumps. They also affect search ranking.
Should I use WebAssembly for everything? No. WebAssembly is for heavy, specific tasks such as image processing, encryption, or large calculations. For normal interface code, JavaScript is simpler, faster to write, and fast enough. Use Wasm only when a measured task is slow in JavaScript and faster in Wasm.
How do I know if my JavaScript bundle is too big? Check its size in your build output and in DevTools, on a mid-range phone with a slow network. As a rough guide, aim for under 200 kilobytes of JavaScript for the first load.
Comments
Post a Comment