Why JavaScript Is Your Biggest Performance Risk
Of all the resources a browser downloads, JavaScript is the most expensive — not just to transfer, but to parse and execute. A 300 KB JavaScript file costs far more in CPU time than a 300 KB image. This directly impacts Core Web Vitals: the set of metrics Google uses to measure real-world user experience and factor into search rankings.
The three Core Web Vitals most affected by JavaScript are:
- LCP (Largest Contentful Paint): Render-blocking scripts delay the main content from appearing.
- INP (Interaction to Next Paint): Heavy JS execution on the main thread delays responses to user input.
- CLS (Cumulative Layout Shift): Scripts that inject content without reserved space cause jarring layout shifts.
Technique 1: Use async and defer Attributes
Adding async or defer to your <script> tags prevents render-blocking — one of the quickest wins available.
defer: Downloads the script in parallel with HTML parsing and executes it after the DOM is ready. Best for most scripts.async: Downloads in parallel and executes as soon as it's ready — regardless of DOM state. Best for independent scripts like analytics.
Rule of thumb: use defer for everything that doesn't need to run before the page renders.
Technique 2: Code Splitting
Don't ship one giant bundle. Use dynamic import() to split your JavaScript into smaller chunks that load on demand:
// Only load the chart library when the user opens the analytics tab
button.addEventListener('click', async () => {
const { renderChart } = await import('./chart.js');
renderChart(data);
});
This keeps your initial page load lean and defers the cost of heavy libraries until they're actually needed.
Technique 3: Serve Libraries from a Public CDN
Loading common libraries (React, Lodash, Chart.js) from a reputable public CDN like jsDelivr or cdnjs offers two benefits:
- Edge delivery: Files are served from a server geographically close to your user.
- Browser cache hits: If another site already loaded the same library version from the same CDN, the file may already be in the user's browser cache — zero download needed.
Technique 4: Tree Shaking and Dead Code Elimination
If you're using a bundler like Rollup or esbuild, enable tree shaking to remove unused code from your final bundle. Import only what you need:
// Bad: imports entire Lodash library
import _ from 'lodash';
// Good: imports only the debounce function
import debounce from 'lodash/debounce';
This alone can reduce bundle sizes dramatically for utility-heavy codebases.
Technique 5: Preload Critical Scripts
Use <link rel="preload"> to tell the browser about critical JavaScript files as early as possible, so it can begin downloading them while still parsing HTML:
<link rel="preload" href="/js/app.js" as="script">
Combine this with a CDN URL for maximum effect — the browser discovers and starts fetching your critical JS immediately, from the closest edge node.
Measuring Your Improvements
After applying these techniques, measure the impact using:
- Chrome DevTools Lighthouse: Run an audit before and after each change.
- WebPageTest.org: Free, detailed waterfall charts showing exactly when each resource loads.
- Google Search Console: Monitor Core Web Vitals scores from real users over time.
Summary
JavaScript performance is iterative. Start with defer/async attributes for immediate wins, then progressively introduce code splitting, CDN delivery, and tree shaking as your project matures. Each technique compounds on the others — and your users (and search rankings) will notice the difference.