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.
-
HTML Basics
- What is HTML: The Structure Layer of Every Web Page
- HTML Document Structure: DOCTYPE, html, head and body
- Elements, Tags and Attributes: The Vocabulary of HTML
- HTML Comments: Notes That Ship With Your Code
- Block Level and Inline Elements
- Writing and Running Your First HTML Page
- How a Browser Turns Markup Into a Page
- Text and Formatting
- Links and Navigation
- Images and Media
- Lists
- Tables
-
Forms
- Form Structure: form, action and method
- Input Types: Text, Email, Number, Date and the Rest
- Labels: The Most Important Element in a Form
- Checkboxes, Radio Buttons and Grouping
- select, option, optgroup and datalist
- textarea, File Uploads and Hidden Fields
- Buttons: submit, reset and button
- Built In Form Validation
- GET or POST: What Happens When a Form Is Submitted
- Semantic HTML
- HTML5 Features
- Head and Metadata
- HTML with CSS
- HTML with JavaScript
- Accessibility
-
HTML SEO
- How Google Works: Crawling, Indexing and Ranking
- SEO Friendly HTML Structure
- Titles and Descriptions That Earn Clicks
- Headings and Content Structure for Search
- Internal Linking and Anchor Text
- robots.txt and XML Sitemaps
- Canonical URLs and Duplicate Content
- Structured Data and JSON-LD
- Image SEO
- Core Web Vitals and Mobile Friendliness
- DevTools and Debugging
- Editor Productivity
- HTML Best Practices
- HTML Projects
- Advanced Projects
- Practice and Exams
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
| Hint | Does | Use for |
|---|---|---|
preconnect | Opens the connection early | An origin you will definitely use |
dns-prefetch | Resolves DNS only | An origin you might use |
preload | Fetches now, at high priority | A critical resource discovered late |
prefetch | Fetches at low priority for a future page | The likely next page |
modulepreload | Preloads a module and its dependencies | Entry 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:
- Do not ship oversized files. A four thousand pixel photograph displayed at eight hundred wastes most of its bytes.
- Compress. Eighty percent JPEG quality is usually indistinguishable and half the size.
- Use modern formats with a fallback.
- Always set dimensions.
- Lazy load below the fold, never above it.
- 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: swapshows 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 revalidateVersioned 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
| Question | Tool |
|---|---|
| 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
- Compress and resize images. Almost always the biggest single win.
- Set image and iframe dimensions.
deferevery script.- Remove JavaScript you do not need.
- Lazy load below the fold, prioritise above it.
- Self host and subset fonts, with
font-display: swap. - Audit third party scripts and remove what is not earning its place.
- 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.
defereverywhere,asynconly 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
- Measure a page with the cache disabled at Fast 3G and record the transferred size.
- Find the three largest resources and reduce each.
- Replace an eagerly loaded video embed with a facade and measure the difference.
- Add
fetchpriority="high"to a hero image and compare the largest paint time.