HTML Performance Attributes
1. Introduction
Modern HTML provides a set of attributes that directly control how the browser loads, parses, and executes resources โ without any JavaScript required. Attributes like defer, async, loading, fetchpriority, decoding, and importance let you express your loading intent directly in HTML, allowing browsers to optimise the critical rendering path. This lesson covers each attribute, when to use it, and the common combinations that significantly improve Core Web Vitals.
2. Concept
Script Loading Attributes Compared
| Attribute | HTML Parsing Blocked? | Execution Order | Best For |
|---|---|---|---|
| (none) | Yes โ immediately | In order | Avoid for external scripts |
defer |
No โ downloads in parallel | In order, after HTML parsed | Non-critical scripts that need DOM |
async |
No โ downloads in parallel | As soon as downloaded (may be out of order) | Independent scripts: analytics, ads |
type="module" |
No โ behaves like defer | In order, deferred | ES module scripts |
defer guarantees scripts execute in the order they appear in HTML, after the document is fully parsed but before DOMContentLoaded. async makes no order guarantees โ the first to download executes first. Use defer for scripts with dependencies; use async for truly independent scripts.<script defer> in the <head>, not at the bottom of <body>. When placed in the head, the browser can begin downloading the script while parsing the HTML. Placing scripts at the bottom of body delays the download start.async scripts may execute before or after DOMContentLoaded, and they execute as soon as they are downloaded โ not when the DOM is ready. Never use async for scripts that need to interact with the DOM or depend on other scripts.3. Basic Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Performance Attributes Demo</title>
<!-- async: independent analytics script, order doesn't matter -->
<script async src="/js/analytics.js"></script>
<!-- defer: main app script, needs DOM, executed in order -->
<script defer src="/js/app.js"></script>
<script defer src="/js/components.js"></script>
<!-- ES module: deferred by default -->
<script type="module" src="/js/main.mjs"></script>
</head>
<body>
<h1>Performance Attributes</h1>
<!-- loading="eager": above-fold image โ default, no lazy loading -->
<img
src="/images/hero.jpg"
alt="Hero image"
width="1200" height="600"
fetchpriority="high"
decoding="sync"
>
<!-- loading="lazy": below-fold image โ deferred until near viewport -->
<img
src="/images/product.jpg"
alt="Product image"
width="400" height="300"
loading="lazy"
decoding="async"
>
<!-- iframe lazy loading -->
<iframe
src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
title="Product demo video"
width="560" height="315"
loading="lazy"
allow="accelerometer; autoplay; clipboard-write; encrypted-media"
allowfullscreen
></iframe>
</body>
</html>
4. How It Works
Step 1 โ defer vs async
defer downloads the script in parallel with HTML parsing and executes it in source order once the HTML is fully parsed. This is safe for all scripts that read or modify the DOM. async also downloads in parallel but executes immediately when downloaded โ potentially mid-parse and out of order. Use async only for truly independent scripts like analytics.
Step 2 โ fetchpriority
fetchpriority="high" on an image signals to the browser’s preload scanner that this resource should be fetched at high priority. Use it on the LCP (Largest Contentful Paint) image โ typically the hero banner. fetchpriority="low" is useful for below-fold images where you want lazy loading to defer even further.
Step 3 โ decoding
decoding="async" allows the browser to decode the image off the main thread without blocking other rendering. decoding="sync" ensures the image is decoded before the next paint โ use this only for above-fold images where you want to avoid a flash of unstyled content. The default is auto (browser decides).
Step 4 โ loading=”lazy” on iframes
The loading="lazy" attribute works on both <img> and <iframe>. Lazy-loading a YouTube embed until it nears the viewport can save several hundred kilobytes of JavaScript and eliminate the render-blocking impact of third-party iframes entirely.
5. Real-World Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TechStore Homepage</title>
<!-- Preconnect to CDN -->
<link rel="preconnect" href="https://cdn.techstore.com" crossorigin>
<!-- Critical CSS inline (omitted here for brevity) -->
<style>/* critical CSS inlined */</style>
<!-- Non-critical CSS loaded asynchronously -->
<link rel="preload" href="/css/full.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/full.css"></noscript>
<!-- analytics: async โ independent, does not need DOM -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXX"></script>
<!-- app scripts: defer โ need DOM and must execute in order -->
<script defer src="/js/polyfills.js"></script>
<script defer src="/js/app.js"></script>
<script defer src="/js/cart.js"></script>
<!-- Preload LCP image -->
<link rel="preload" as="image" href="/banners/hero.webp" imagesrcset="/banners/hero-480.webp 480w, /banners/hero.webp 1200w" imagesizes="(max-width:480px) 100vw, 1200px">
</head>
<body>
<!-- LCP image: fetchpriority high, no lazy loading -->
<picture>
<source type="image/webp" srcset="/banners/hero-480.webp 480w, /banners/hero.webp 1200w" sizes="(max-width:480px) 100vw, 1200px">
<img
src="/banners/hero.jpg"
alt="TechStore summer sale โ up to 40% off laptops and monitors"
width="1200" height="400"
fetchpriority="high"
decoding="sync"
>
</picture>
<h1>Summer Sale โ Up to 40% Off</h1>
<!-- Below-fold products: lazy load -->
<section>
<h2>Featured Products</h2>
<img src="/products/probook15-thumb.jpg" alt="ProBook 15 laptop thumbnail" width="300" height="200" loading="lazy" decoding="async">
<img src="/products/monitor4k-thumb.jpg" alt="4K monitor thumbnail" width="300" height="200" loading="lazy" decoding="async">
<img src="/products/keyboard-thumb.jpg" alt="Mechanical keyboard thumbnail" width="300" height="200" loading="lazy" decoding="async">
</section>
<!-- Third-party embed: lazy load to avoid blocking render -->
<iframe
src="https://www.youtube-nocookie.com/embed/PRODUCT_VIDEO_ID"
title="ProBook 15 overview video"
width="560" height="315"
loading="lazy"
allow="accelerometer; autoplay; encrypted-media"
allowfullscreen
style="border:none"
></iframe>
</body>
</html>
6. Common Mistakes
❌ Loading scripts without defer or async โ blocks HTML parsing
<head>
<script src="/js/app.js"></script> <!-- blocks rendering -->
</head>
✓ Use defer in the head for DOM-dependent scripts
<head>
<script defer src="/js/app.js"></script>
</head>
❌ async on a script that depends on another script
<script async src="/js/jquery.js"></script>
<script async src="/js/jquery-plugin.js"></script> <!-- may run before jQuery -->
✓ Use defer for scripts with dependencies; async only for independent scripts
<script defer src="/js/jquery.js"></script>
<script defer src="/js/jquery-plugin.js"></script> <!-- guaranteed order -->
7. Try It Yourself
▶ Test with PageSpeed Insights
8. Quick Reference
| Attribute | Element | Effect | Use When |
|---|---|---|---|
| defer | script | Download in parallel; execute in order after parse | DOM-dependent scripts |
| async | script | Download in parallel; execute immediately when ready | Independent scripts (analytics) |
| loading=”lazy” | img, iframe | Defers load until near viewport | Below-fold images and iframes |
| fetchpriority=”high” | img, link, script | Elevates download priority | LCP image, critical CSS |
| decoding=”async” | img | Decode off main thread | Below-fold images |
| decoding=”sync” | img | Decode before next paint | Above-fold hero image |
| type=”module” | script | ES module โ deferred, strict mode | Modern JS modules |