← Back to Course Index

Module 2.4 — Resource Optimization

Phase: Phase 2 — Performance Engineering (Core Web Vitals)

Resource optimization is the engineering discipline of making sure every asset a browser fetches — images, fonts, scripts, stylesheets, and third-party requests — is as small as possible, arrives as early as needed, and does not block the browser from rendering visible content. It is one of the highest-leverage levers you have for improving LCP, INP, and CLS simultaneously.

This module covers every major resource category with a practical, metric-tied approach. For each category you will learn what the problem looks like in tooling, what the correct fix is, and how to verify the improvement.


1. Why Resources Are a Core Web Vitals Problem

Before diving into individual resource types, build a mental model of how resources connect to each metric:

Resource optimization is therefore not a generic "speed" exercise — every technique in this module maps to at least one of those three metrics.


2. Image Optimization

Images are consistently the largest contributors to page weight and LCP delay. They are also the most approachable optimization target.

2.1 Choosing the Right Format

Modern browsers support multiple image formats. Choosing the right one can cut file size by 30–80% without visible quality loss.

Serve format variants using the <picture> element with multiple <source> elements so the browser selects the best supported format:

<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" alt="A descriptive alt text" width="1200" height="630">
</picture>

2.2 Responsive Images

Serving a 2400px-wide image to a 375px mobile screen wastes bandwidth and slows LCP. Responsive images solve this by letting the browser choose the most appropriate source from a set of candidates.

The srcset attribute lists image candidates with their intrinsic widths. The sizes attribute tells the browser how wide the image will be displayed at each viewport size, before CSS loads. The browser uses both to calculate which source to download.

<img
  src="hero-800.jpg"
  srcset="hero-400.jpg 400w,
          hero-800.jpg 800w,
          hero-1600.jpg 1600w"
  sizes="(max-width: 600px) 100vw,
         (max-width: 1200px) 80vw,
         1200px"
  alt="Hero image"
  width="1200"
  height="630"
>

Always specify width and height attributes. This allows the browser to calculate the image's aspect ratio before it loads, reserving the correct space and eliminating image-caused CLS. CSS aspect-ratio achieves the same result if the attributes are impractical.

2.3 Loading Strategy: Lazy vs Eager

loading="lazy" defers off-screen images until the user scrolls near them. It is almost always correct for images below the fold.

Critical exception: never lazy-load the LCP image. If the LCP element is an <img>, adding loading="lazy" delays the browser's download of it, directly and severely harming your LCP score. Above-the-fold images should always use loading="eager" (the default) or be actively preloaded.

<!-- WRONG: lazy-loading the hero image delays LCP -->
<img src="hero.jpg" loading="lazy" alt="Hero">

<!-- CORRECT: eager is the default; state it explicitly if helpful -->
<img src="hero.jpg" loading="eager" fetchpriority="high" alt="Hero">

<!-- CORRECT: lazy-load images that start off-screen -->
<img src="card-thumbnail.jpg" loading="lazy" width="400" height="300" alt="Card">

2.4 Fetch Priority and Preload for LCP Images

The fetchpriority="high" attribute (also settable via a <link rel="preload"> hint) signals to the browser that this resource is critical and should be downloaded before lower-priority resources. Use it on the LCP element.

<!-- Preload a hero image discovered late (e.g. as a CSS background) -->
<link rel="preload" as="image" href="hero.avif" type="image/avif"
  imagesrcset="hero-400.avif 400w, hero-800.avif 800w, hero-1600.avif 1600w"
  imagesizes="(max-width: 600px) 100vw, 1200px"
>

Preloading is especially important when the LCP element is a CSS background image, because the browser cannot discover CSS background images until it has built the CSSOM, which can be very late in the rendering pipeline. A <link rel="preload"> in the <head> lets the browser start the download immediately.

2.5 Image Compression

Format choice gets you most of the savings, but lossless compression and quality tuning finish the job. Target the lowest quality setting at which the image is indistinguishable to the eye at its displayed size. Tools:


3. Resource Hints: Preconnect, Preload, Prefetch

Resource hints are instructions in the <head> that let the browser take early action on critical resources before it discovers them in the main document or CSS.

3.1 preconnect

Establishes a connection (DNS lookup, TCP handshake, TLS negotiation) to an origin before it is needed. Use it for critical third-party origins whose resources block rendering — fonts, CDN, analytics.

<!-- Preconnect to the fonts CDN before the stylesheet requests the font files -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

The crossorigin attribute is required for origins that serve CORS-enabled resources (fonts, scripts), because the browser opens a separate CORS connection.

Do not preconnect to every third party. Each preconnect consumes CPU and network resources. Limit it to 2–4 origins that are genuinely in the critical rendering path.

3.2 preload

Forces the browser to discover and download a specific resource at the highest priority, before it would normally find it. Use for:

<link rel="preload" as="font" href="/fonts/inter-var.woff2" type="font/woff2" crossorigin>
<link rel="preload" as="image" href="/hero.avif">
<link rel="preload" as="style" href="/critical.css">

Warning: preloading a resource you do not use within a few seconds generates a browser warning and wastes bandwidth. Only preload what is needed for the initial visible render.

3.3 prefetch

Downloads a resource at low priority in the background for use on a future navigation. Appropriate for resources needed on the next page a user is likely to visit (e.g. prefetch the JavaScript bundle for a page linked in a prominent CTA). It does not help current-page performance and should not be confused with preload.

<link rel="prefetch" href="/checkout/bundle.js" as="script">

3.4 dns-prefetch

A lighter-weight fallback to preconnect: performs only the DNS lookup, not the TCP/TLS handshake. Useful for third-party origins that load below the fold where a full preconnect would waste resources.

<link rel="dns-prefetch" href="https://www.googletagmanager.com">

4. Code Splitting and Tree Shaking

Modern JavaScript applications bundle many modules together. Sending the entire application bundle to every page is wasteful and is a primary cause of long tasks (INP) and slow initial rendering (LCP).

4.1 Code Splitting

Code splitting divides a JavaScript bundle into smaller chunks that are loaded only when needed. Instead of one large main.js file, the user downloads only the code required for the current route or interaction.

In React / Next.js, route-based code splitting is automatic — each page gets its own bundle. You can push this further with dynamic imports to defer loading of heavy components until a user interacts with them:

// Without code splitting — the entire modal JS loads immediately
import HeavyModal from './HeavyModal';

// With dynamic import — HeavyModal is fetched only when needed
import dynamic from 'next/dynamic';
const HeavyModal = dynamic(() => import('./HeavyModal'), { ssr: false });

In plain JavaScript, you can use import() (dynamic import syntax) to lazily load a module:

button.addEventListener('click', async () => {
  const { initChart } = await import('./chart.js');
  initChart(data);
});

4.2 Tree Shaking

Tree shaking is a build-time optimization where the bundler (Webpack, Rollup, esbuild, Vite) statically analyses your import statements and removes code that is imported but never called. It only works with ES module syntax (import/export), not CommonJS (require).

Common tree-shaking failure modes:

Use the Webpack Bundle Analyzer or Vite's rollup-plugin-visualizer to see which modules are contributing most to your bundle size, then target the largest offenders.


5. Deferring Third-Party Scripts

Third-party scripts (analytics, tag managers, chat widgets, A/B testing tools, ad networks, social embeds) are among the most common causes of INP regressions and LCP delays. They run code you do not control on the main thread you share with your users.

5.1 Load Strategy

The correct strategy for nearly every third-party script is to load it with defer or async, placed at the bottom of <body> or injected after the page's critical content has painted.

<!-- Deferred analytics — will not block parsing or rendering -->
<script src="https://www.googletagmanager.com/gtag/js?id=G-XXXX" defer></script>

5.2 Facade Pattern

For particularly heavy third-party embeds (YouTube players, chat widgets, maps), use a facade: render a static placeholder image or button that looks like the embed, and only load the real embed when the user clicks or interacts with it. This defers all the third-party network requests and script execution until they are actually needed.

// Pseudocode: facade for a YouTube embed
const facade = document.getElementById('video-facade');
facade.addEventListener('click', () => {
  const iframe = document.createElement('iframe');
  iframe.src = `https://www.youtube.com/embed/${videoId}?autoplay=1`;
  facade.replaceWith(iframe);
});

Next.js provides a <Script> component with a strategy prop that handles this automatically:

import Script from 'next/script';

<Script
  src="https://www.googletagmanager.com/gtag/js?id=G-XXXX"
  strategy="afterInteractive"
/>

5.3 Audit Your Third-Party Impact

In Lighthouse, the "Reduce the impact of third-party code" audit lists every third-party origin, its total blocking time contribution, and its transfer size. In WebPageTest, the waterfall view shows third-party requests against your own resources so you can see which origins delay your LCP element. Use these reports to decide what to defer, facade, or remove entirely.


6. Critical CSS

CSS is render-blocking by default. The browser cannot paint anything until it has downloaded and processed every stylesheet in the <head>. On slow connections, large stylesheets are a direct LCP cause.

The solution is a two-step strategy:

  1. Extract critical CSS — the styles required to render the above-the-fold content — and inline them in a <style> block in the <head>. The browser can now paint the initial visible area without any external stylesheet request.
  2. Load the full stylesheet non-blocking by using a media attribute trick or the rel="preload" pattern:
<!-- Inline critical styles -->
<style>
  body { margin: 0; font-family: sans-serif; }
  .hero { background: #1a1a2e; color: #fff; padding: 4rem 2rem; }
</style>

<!-- Load the full stylesheet without blocking render -->
<link rel="preload" href="/styles/main.css" as="style"
      onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles/main.css"></noscript>

Tools for automated critical CSS extraction: Critical (npm), Penthouse, and build-pipeline integrations such as the Gatsby plugin or Next.js custom setups. Many CDN-level performance platforms (Cloudflare, Fastly) can do this automatically.


7. Web Font Optimization

Web fonts are a frequent LCP and CLS cause. The browser must download a font file before it can render text that uses it — and if the font takes too long, text either disappears (FOIT, Flash of Invisible Text) or renders in the fallback font and then shifts (FOUT, Flash of Unstyled Text), causing CLS.

7.1 font-display

The font-display CSS descriptor controls how the browser behaves while the font is loading:

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

7.2 Self-Hosting Fonts

Hosting fonts on your own origin (or CDN) eliminates a third-party DNS lookup and connection. Combine with a <link rel="preload"> for the woff2 file to get the browser downloading it at the highest priority, before it encounters the @font-face rule in the stylesheet.

<link rel="preload" as="font" href="/fonts/inter-var.woff2"
      type="font/woff2" crossorigin>

7.3 Use Variable Fonts

A variable font encodes all weights and styles in a single file. Instead of loading separate woff2 files for regular, bold, and italic, you load one file. This reduces both request count and total transfer size.

7.4 Reduce CLS from Font Swap

Even with font-display: swap, the layout shift that occurs when the custom font loads and differs in size from the fallback contributes to CLS. The CSS size-adjust descriptor (and the related ascent-override, descent-override, line-gap-override) allow you to adjust the fallback font's metrics to match the custom font so the swap causes no visible shift.

@font-face {
  font-family: 'Inter-fallback';
  src: local('Arial');
  size-adjust: 96.7%;
  ascent-override: 90%;
}

body {
  font-family: 'Inter', 'Inter-fallback', sans-serif;
}

8. Compression: Brotli and Gzip

HTTP compression reduces the transfer size of text-based resources — HTML, CSS, JavaScript, JSON, SVG — over the network. It is a server/CDN configuration concern, but you must understand it to audit it.

To verify compression is active, inspect the response headers in Chrome DevTools → Network tab → select any HTML/CSS/JS resource → check Content-Encoding:

Content-Encoding: br       ← Brotli active
Content-Encoding: gzip     ← Gzip active
(no header)                ← No compression — investigate

On nginx: enable brotli on; and gzip on;. On Cloudflare, Brotli is on by default. On Vercel and Netlify, Brotli is applied automatically. Check your CDN's documentation — enabling compression at the CDN level requires zero application code changes and yields immediate transfer-size reductions across the entire site.


9. HTTP/2 and HTTP/3

The HTTP protocol version affects how efficiently resources are transferred from server to browser.

Verify your protocol in DevTools → Network tab → right-click the column headers → enable "Protocol". You should see h2 or h3. If you see http/1.1, upgrade your server or move behind a CDN that handles the protocol upgrade for you.


10. Putting It Together: Resource Optimization Audit Workflow

Use this structured workflow when auditing any URL for resource optimization issues:

  1. Identify the LCP element — use the Performance panel in Chrome DevTools or Lighthouse. Note whether it is an image, background image, text, or video poster.
  2. Trace the LCP resource's critical path — is it preloaded? What is its format and size? Is fetchpriority="high" set? Is it lazy-loaded by mistake?
  3. Check image formats and sizes — open the Network tab, filter by "Img". Are formats WebP or AVIF? Are images resized close to their displayed dimensions?
  4. Audit fonts — filter Network by "Font". Are fonts preloaded? Is font-display: swap set? Are they self-hosted?
  5. Check compression — filter by "Doc", "CSS", "JS". Inspect Content-Encoding response headers.
  6. Review third-party impact — in Lighthouse, check "Reduce the impact of third-party code". In Network tab, enable the "Domain" column to identify third-party origins.
  7. Inspect script loading — filter by "JS". Are render-blocking scripts present? Do they have defer or async?
  8. Check bundle size — are there large monolithic JavaScript bundles that should be split?
  9. Verify resource hints — inspect the <head> (View Source). Are preconnect hints present for critical origins? Any unnecessary preloads?
  10. Measure before and after — run WebPageTest with film strip view before making changes. Implement fixes, then re-run and compare LCP, Total Blocking Time, and CLS numbers.

Key Takeaways


Milestone Task

Select a real page that has at least one "Needs Improvement" or "Poor" Core Web Vitals metric. Using the audit workflow above:

  1. Identify every resource that is contributing to the LCP delay, CLS, or INP.
  2. For each resource issue found, write the exact fix (code snippet, configuration change, or tool setting).
  3. Implement at least three of the fixes on a staging environment.
  4. Re-run Lighthouse and WebPageTest. Document the before and after scores, tying each improvement to the specific resource change that caused it.

You have completed this module when you can explain, without looking at notes, exactly how each technique above maps to LCP, CLS, or INP — and when your audit produces measurable before/after improvements in at least two metrics.