Performance

Frontend Performance Tips That Actually Work

frontend-performance-tips

Last month I ran Lighthouse on a client's site. Score: 34. The page loaded 4MB of JavaScript, three render-blocking stylesheets, eighteen unoptimized PNG images, and four custom fonts — all above the fold. The fix? Nothing exotic. Just fundamentals that somehow got skipped. Every technique in this article is something I've personally used to push real sites from "painfully slow" to "fast enough that nobody complains."

No theoretical fluff. Just what works.

Understand What You're Measuring: Core Web Vitals

Before you optimize anything, know what Google actually measures. Three metrics matter most:

Everything below maps to improving one or more of these. If you're ever asked "what should I optimize first?" — measure these three and fix whichever is worst.

Image Optimization: The Biggest Win

Images account for roughly 50% of page weight on the average website. This is where you get the most improvement for the least effort.

Use Modern Formats

Stop serving PNG and JPEG for photos. Use WebP or AVIF. The savings are dramatic — AVIF is typically 50% smaller than JPEG at equivalent quality. WebP is about 25-30% smaller.

<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" alt="Hero banner" width="1200" height="600"
       loading="lazy" decoding="async">
</picture>

The <picture> element lets browsers pick the best format they support. Everyone gets the smallest file they can handle.

Always Set Width and Height

This one's embarrassingly simple, but it fixes CLS. When you specify width and height on an <img> tag, the browser reserves space before the image loads. No layout shift.

/* Combine with this CSS for responsive sizing */
img {
  max-width: 100%;
  height: auto;
}

Lazy Load Below-the-Fold Images

Native lazy loading is a single attribute:

<img src="photo.webp" loading="lazy" alt="Product shot">

The browser won't fetch it until it's near the viewport. But don't lazy load your LCP image — that hero banner at the top needs to load immediately. In fact, preload it:

<link rel="preload" as="image" href="hero.webp" type="image/webp">

Tip: Use fetchpriority="high" on your LCP image to tell the browser it's important. This attribute can improve LCP by 100-400ms in real-world tests. Pair it with a preload link for maximum effect.

Font Optimization: Death by a Thousand Requests

Custom fonts are one of those things that look innocent until you profile them. A single Google Fonts include can trigger 3-4 HTTP requests and block rendering for 200ms+.

Self-Host Your Fonts

Download the font files, convert to WOFF2 (the smallest format), and host them yourself. You eliminate the DNS lookup and connection to Google's CDN:

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap;
  font-style: normal;
}

That font-display: swap is critical. It tells the browser to show text in a system font immediately, then swap in the custom font when it loads. Without it, you get a flash of invisible text (FOIT) — users stare at blank space.

Subset Your Fonts

If you only use Latin characters, don't ship Cyrillic, Greek, and Vietnamese glyphs. Tools like glyphhanger or pyftsubset can strip unused characters. I've seen font files go from 250KB to 30KB after subsetting.

Limit Font Weights

Every weight is another file. Do you really need Regular, Medium, SemiBold, Bold, and ExtraBold? In my experience, two weights — Regular and Bold — cover 90% of designs. Use variable fonts if you need more flexibility; one file handles all weights.

JavaScript: Load Less, Load Later

JavaScript is the most expensive resource on the web. Not just in file size, but in parsing and execution time. A 200KB script takes far longer to process than a 200KB image.

Code Splitting

Don't ship your entire app in one bundle. Split by route at minimum:

// React example with lazy loading
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

Users who never visit Settings never download that code. The savings compound as your app grows.

Defer Third-Party Scripts

Analytics, chat widgets, social media embeds — they all add up. Load them after your content:

<!-- Bad: blocks rendering -->
<script src="https://analytics.example.com/tracker.js"></script>

<!-- Good: loads after HTML parsing -->
<script defer src="https://analytics.example.com/tracker.js"></script>

<!-- Also good: loads in parallel, executes when ready -->
<script async src="https://analytics.example.com/tracker.js"></script>

Use defer for scripts that need DOM access (they execute in order, after parsing). Use async for independent scripts like analytics (they execute as soon as they download, in any order).

Tip: Run npx bundlephobia <package-name> before adding any npm package. A "small utility library" can easily be 50KB gzipped. moment.js is 72KB. date-fns with tree-shaking is 3KB for most use cases. Know what you're shipping.

CSS: The Sneaky Blocker

CSS is render-blocking by default. The browser won't paint anything until it's parsed all your CSS. If your stylesheet is 300KB, that's a problem.

Inline Critical CSS

Extract the CSS needed for above-the-fold content and inline it directly in the <head>:

<head>
  <style>
    /* Only the CSS needed for initial render */
    body { font-family: sans-serif; margin: 0; }
    .hero { padding: 60px 20px; }
    .nav { position: fixed; top: 0; width: 100%; }
  </style>
  <!-- Full stylesheet loads asynchronously -->
  <link rel="preload" href="/styles.css" as="style"
        onload="this.onload=null;this.rel='stylesheet'">
</head>

The browser renders immediately with the inlined CSS, then loads the full stylesheet in the background. Users see content faster.

Remove Unused CSS

Tools like PurgeCSS scan your HTML/JS and strip any CSS selectors that aren't used. I've seen projects where 80% of the CSS was dead code from frameworks like Bootstrap. Removing it cut the stylesheet from 180KB to 25KB.

Caching: Make Return Visits Instant

Once someone visits your site, subsequent visits should be near-instant. Caching makes this happen.

Set Aggressive Cache Headers

# Static assets with content hashes — cache forever
Cache-Control: public, max-age=31536000, immutable

# HTML pages — always revalidate
Cache-Control: no-cache

# API responses — cache briefly
Cache-Control: public, max-age=60, s-maxage=300

The key insight: use content hashes in your filenames (app.a1b2c3.js). When the content changes, the filename changes. You can cache the old file forever because it'll never be requested again once the new version deploys.

Service Workers for Offline-First

For apps that need to work offline or on flaky connections, a service worker intercepts network requests and serves cached responses:

// Simple cache-first strategy
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return cached || fetch(event.request).then((response) => {
        const clone = response.clone();
        caches.open('v1').then(cache => cache.put(event.request, clone));
        return response;
      });
    })
  );
});

I won't pretend service workers are simple — they add complexity. But for the right use case (PWAs, documentation sites, dashboards), they're incredibly powerful.

Quick Wins Checklist

Before you dive into complex optimizations, knock out these freebies:

  1. Enable gzip/Brotli compression on your server. Brotli compresses 15-20% better than gzip.
  2. Use a CDN. Serve static assets from edge servers close to your users.
  3. Preconnect to required origins. If you load resources from third-party domains, tell the browser early: <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  4. Minify everything. HTML, CSS, and JS. Your bundler probably does this, but check.
  5. Avoid layout thrashing. Don't read DOM dimensions and write styles in a loop. Batch your reads, then batch your writes.
  6. Use will-change sparingly. It hints to the browser that an element will animate, allowing GPU layer promotion. But overuse it and you'll eat memory.

Optimize Your HTML Templates

Convert your optimized HTML layouts to images for performance documentation, sharing, and visual regression testing.

Try HTML to PNG Converter →

Measure, Don't Guess

The biggest performance mistake isn't a specific technique — it's optimizing without measuring. I've watched developers spend days shaving 10KB off a JavaScript bundle while ignoring the 2MB hero image loading synchronously above the fold.

Use Lighthouse for lab data. Use Chrome's CrUX dashboard or PageSpeed Insights for real-user data. Use the Performance tab in DevTools to trace exactly what's happening during load. Find the bottleneck, fix it, measure again.

Performance work isn't glamorous. It's not a new framework or a clever architecture. It's the boring stuff — right image formats, proper caching, loading scripts smartly, not shipping code you don't need. But it's the boring stuff that makes your site feel fast. And fast sites win.

Sachin Bhanushali
Written by

Sachin Bhanushali

Full-stack developer and creator of HTMLtoImages. Building free, privacy-first developer tools that run entirely in your browser.