HTML Performance Best Practices

What the markup alone can do for speed, with no build tooling: resource hints, image discipline, script loading and the small set of attributes that move the measurements.

Concept

A large share of page speed is decided in the HTML, before any bundler or framework is involved. This note collects the markup level decisions in one place.

The head, in order

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <!-- start the connection to a third party origin early -->
  <link rel="preconnect" href="https://fonts.example.com" crossorigin>
  <link rel="dns-prefetch" href="https://cdn.example.com">

  <!-- the one image that matters most -->
  <link rel="preload" as="image" href="/images/hero-800.jpg"
        imagesrcset="/images/hero-400.jpg 400w, /images/hero-800.jpg 800w"
        imagesizes="100vw" fetchpriority="high">

  <title>Design course - Riverside College</title>
  <meta name="description" content="...">
  <link rel="canonical" href="https://example.edu/courses/design">

  <link rel="stylesheet" href="/styles/site.css">
  <script src="/scripts/app.js" defer></script>
</head>

Resource hints

HintDoesUse for
preconnectOpens the connection earlyAn origin you will definitely use
dns-prefetchResolves DNS onlyAn origin you might use
preloadFetches now, at high priorityA critical resource discovered late
prefetchFetches at low priority for a future pageThe likely next page
modulepreloadPreloads a module and its dependenciesEntry modules

Two cautions. preconnect to more than about four origins is counterproductive - each connection costs resources. And a preload that is never used is pure waste; the console warns about it.

Images

Usually the largest part of a page.

<!-- above the fold: eager, high priority, dimensions set -->
<img src="/images/hero-800.jpg"
     srcset="/images/hero-400.jpg 400w, /images/hero-800.jpg 800w, /images/hero-1600.jpg 1600w"
     sizes="100vw"
     alt="The main gate at sunrise"
     width="1600" height="900"
     fetchpriority="high" decoding="async">

<!-- below the fold: lazy -->
<img src="/images/gallery-1.jpg"
     alt="A stall selling brass lamps"
     width="800" height="600"
     loading="lazy" decoding="async">

<!-- modern formats with a fallback -->
<picture>
  <source type="image/avif" srcset="/images/lamp.avif">
  <source type="image/webp" srcset="/images/lamp.webp">
  <img src="/images/lamp.jpg" alt="A brass lamp" width="1200" height="800" loading="lazy">
</picture>

The rules in order of impact:

  1. Do not ship oversized files. A four thousand pixel photograph displayed at eight hundred wastes most of its bytes.
  2. Compress. Eighty percent JPEG quality is usually indistinguishable and half the size.
  3. Use modern formats with a fallback.
  4. Always set dimensions.
  5. Lazy load below the fold, never above it.
  6. Use SVG for anything drawn.

Scripts

<script src="/scripts/app.js" defer></script>              <!-- the default choice -->
<script src="/scripts/analytics.js" async></script>        <!-- independent only -->
<script type="module" src="/scripts/main.js"></script>     <!-- deferred automatically -->

A blocking script in the head stops the parser for its entire download and execution. On a slow connection that is the whole page, blank, for seconds.

The larger question is how much JavaScript is there at all. A framework loaded to render a mostly static page is usually the single largest performance cost on it.

Fonts

@font-face {
  font-family: "Inter";
  src: url("/fonts/inter.woff2") format("woff2");
  font-display: swap;
  size-adjust: 100%;        /* match the fallback metrics to reduce shift */
  unicode-range: U+0000-00FF;
}
  • font-display: swap shows fallback text immediately rather than hiding it.
  • WOFF2 only. Older formats are unnecessary weight.
  • Subset to the characters you use.
  • Self host rather than pulling from a third party origin; it removes a connection and a privacy question.
  • Two weights is usually enough. Each additional file is another request.

Third party content

Every embedded widget, tag manager and social button is another origin, another connection and another script running with full access to your page.

<!-- lazy load embeds -->
<iframe src="https://www.youtube-nocookie.com/embed/VIDEO_ID"
        title="Campus tour" loading="lazy" allowfullscreen></iframe>

Better still, use a facade: show a poster image with a play button and load the real embed only when someone clicks. An embedded video player is several hundred kilobytes, and on most pages nobody presses play.

Caching and compression

Server side, but the markup decides what can be cached well.

/styles/site.a3f9c1.css     versioned filename, cache for a year
/scripts/app.7b2e04.js      versioned filename, cache for a year
/index.html                 short cache, must revalidate

Versioned asset names allow a long cache lifetime with no risk of serving stale files, because a change produces a new name.

Ensure the server sends compressed responses - Brotli or gzip - for HTML, CSS, JavaScript and SVG. It is a configuration line and typically cuts text transfer by seventy percent.

Measuring

QuestionTool
What did real visitors experience?Search Console Core Web Vitals
Why is this URL slow?PageSpeed Insights
What is being downloaded?DevTools Network panel
What is blocking the first paint?Network waterfall
What is the main thread doing?Performance panel

Measure with the cache disabled and throttling on. A fast connection and a warm cache flatter every page.

A short priority list

  1. Compress and resize images. Almost always the biggest single win.
  2. Set image and iframe dimensions.
  3. defer every script.
  4. Remove JavaScript you do not need.
  5. Lazy load below the fold, prioritise above it.
  6. Self host and subset fonts, with font-display: swap.
  7. Audit third party scripts and remove what is not earning its place.
  8. Enable compression and long cache lifetimes on versioned assets.

Important rules

  • Stylesheets block painting; scripts block parsing.
  • Never lazy load the largest above the fold image.
  • An unused preload is wasted bandwidth and a console warning.
  • Dimensions on images and iframes prevent layout shift.
  • Field data matters more than a lab score.

Common mistakes

  • Uncompressed multi megabyte images.
  • A blocking script in the head.
  • Lazy loading the hero.
  • Preloading everything.
  • Six font files for one page.
  • Measuring with a warm cache on a fast connection.
  • Optimising markup while ignoring a two megabyte JavaScript bundle.

Best practices

  • Set a page weight budget and check it in the Network panel.
  • Compress and resize images before they enter the repository.
  • Dimensions on every image and iframe.
  • defer everywhere, async only for independent scripts.
  • Facades for heavy embeds.
  • Version asset file names and cache them for a long time.
  • Test on a throttled connection and a real phone.

Practice

  1. Measure a page with the cache disabled at Fast 3G and record the transferred size.
  2. Find the three largest resources and reduce each.
  3. Replace an eagerly loaded video embed with a facade and measure the difference.
  4. Add fetchpriority="high" to a hero image and compare the largest paint time.

Useful resources

Hand picked references for this topic
Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All HTML notes →
HTML

Project Exams

Five larger assessments where the deliverable is a working site. Requirements, constraints and a rubric for each.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.