completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

Embeds and facades

What a video player, a map and a chat widget cost before anyone uses them, and how facades, lazy iframes and reserved space keep embeds cheap.

By the end, you can
  • 01
    Count what an embed costsRead the bytes, requests and main-thread work an embed brings before anyone touches it.
  • 02
    Build a facadeShow a light look-alike for a video, a map or a chat widget, load the real one on a tap, and know what that tap costs.
  • 03
    Lazy load and reserve spaceHold back iframes far down with loading="lazy", and keep each embed's space ready with aspect-ratio.

A video, a map and a chat on the mint tea page

Ravi's mint tea page has a photo, a few words and the brewing steps. This week he adds three things from other sites. Under "How to brew", the brewing video, from the shop's channel on a video site. Far down, under "Find us", a map to the shop. And in the corner, a chat bubble that says "Ask Ravi".

Each one is an embed: content from another site, shown inside his page. Each took only a few lines, copied from that site. Asha opens the page on her phone, on a slow mobile link. She reads about the tea and leaves. She never plays the video, never scrolls to the map, and never opens the chat. What did those three things cost her?

The course didn't load the real YouTube player, Google Maps or a chat service. web.dev warns that "embed source code may change". Then the course's numbers couldn't be checked again later. And the course keeps its test browser off other companies' servers. So it built its own stand-ins: a video player, a map and a chat widget. Each is about as heavy as the real ones its sources describe. The next section says exactly what they are.

The course loaded the page three times in Chrome 157, on a phone's screen. It used DevTools' Slow 4G network, and made the CPU 4 times slower. Then it loaded a second version, where each embed had a light look-alike in its place. That look-alike is called a facade.

What the page cost before Asha tapped anythingPop out in the code editor (opens in a new tab)JavaScript
// Asha's phone, before she tapped anything, as recorded (the median of 3 loads)const embeds = { bytes: 1092093, requests: 24, blockingMs: 185, loadEventMs: 7970 };const facades = { bytes: 18395, requests: 3, blockingMs: 0, loadEventMs: 1280 }; console.log(`Real embeds: ${embeds.bytes} bytes in ${embeds.requests} requests, ${embeds.blockingMs} ms of blocking`);console.log(`Facades: ${facades.bytes} bytes in ${facades.requests} requests, ${facades.blockingMs} ms of blocking`);console.log(`Load event: ${embeds.loadEventMs} ms with real embeds, ${facades.loadEventMs} ms with facades`);
  • Line 1 is a comment. Each number is the median of three loads: the middle one once they're sorted. Times are in milliseconds (ms) after Asha opened the page.
  • Line 2 is the page with the real embeds. It holds the bytes Asha downloaded and the requests her browser made. It also holds how long her page was blocked: too busy running scripts to answer a tap. The last number is when the load event came: the moment the page and all its files had finished loading. Line 3 is the page with facades.
  • Line 5 prints Real embeds: 1092093 bytes in 24 requests, 185 ms of blocking.
  • Line 6 prints Facades: 18395 bytes in 3 requests, 0 ms of blocking.
  • Line 7 prints Load event: 7970 ms with real embeds, 1280 ms with facades.

With the real embeds, Asha downloaded more than a megabyte, for three things she never used. Her page was blocked for 185 ms, and its load event came more than 6 seconds later. With facades, she downloaded 18,395 bytes. Here is what her screen showed:

The mint tea page on Asha's phone at its first paint, when the photo arrived, and when everything had loaded. The real embeds: 673 ms, words, an empty box; 1,333 ms, the photo, an empty box; 7,988 ms, the player and the chat. Facades: 666 ms, words, a Play button; 1,289 ms, the photo, the thumbnail; 1,289 ms, nothing more to load.First paintThe photoAll loadedThe real embedsThe mint tea page at 673 ms: words, an empty box673 mswords, an empty boxThe mint tea page at 1,333 ms: the photo, an empty box1,333 msthe photo, an empty boxThe mint tea page at 7,988 ms: the player and the chat7,988 msthe player and the chatFacadesThe mint tea page at 666 ms: words, a Play button666 mswords, a Play buttonThe mint tea page at 1,289 ms: the photo, the thumbnail1,289 msthe photo, the thumbnailThe mint tea page at 1,289 ms: nothing more to load1,289 msnothing more to load
Asha's screen as Chrome drew it, before she touched anything. With the real embeds, the video's box was still empty when the photo came. With facades, everything was there with the photo.

Look at the video's box in the top row, the page with the real embeds. It was still empty when the photo arrived. The chat bubble came at almost 6 seconds, and the player after about 7. In the bottom row, a Play button was there from the first paint. A picture of the video appeared behind it, at the same moment as the photo. That button and picture are the facade.

Real-life analogyA display model in the shop window

A phone shop doesn't switch on a real phone of every model in its window. It puts out dummies: they look exactly like the phones, but they're empty shells. When you want one, the assistant fetches a real phone from the back. You wait a minute, but only the people who buy one wait.

In real life: The dummy phone in the window
In the page: The facade: a picture of the player and a Play button
In real life: Fetching a real phone from the back
In the page: Loading the real player after a tap
In real life: The minute you wait at the counter
In the page: The wait after the tap
In real life: Window shoppers who walk on
In the page: Visitors who never tap: they download almost nothing
The main idea

An embed brings all its files and all its work, whether the visitor uses it or not. Show a light look-alike until someone asks for it. Lazy load the ones far down. Keep each one's space ready. A facade moves the cost to the visitor who taps it. So use it for embeds that are a choice, not for the reason the page exists.

The rest of the lesson takes four questions, one section each:

  • What does an embed load? What a typical embed loads.
  • How does a facade work, and what does it cost? Facades for video, maps and chat.
  • What about embeds far down the page? Lazy-loading iframes.
  • How do you stop an embed pushing the page around? Reserving space.

What a typical embed loads

An embed is content from another site, shown inside your page. web.dev describes it as content that's "Not authored by you" and "Served from third-party servers". A third party is any site that isn't yours. Ravi's three embeds came as three snippets:

Ravi's three embeds, as each site gives themHTML
<iframe class="video" src="https://player.example/embed/brew" title="How to brew mint tea" width="560" height="315" allow="autoplay; fullscreen"></iframe>
<iframe class="map" src="https://maps.example/embed?place=leaf-and-kettle" title="Map: Leaf and Kettle" width="600" height="450"></iframe>
<script src="https://chat.example/widget.js" async></script>
  • Line 1 is the video. An iframe is a box that shows a whole page from another address inside yours. This one comes from player.example. The width and height are the size the site suggests, and title names the box for screen readers.
  • Line 2 is the map: another iframe, from maps.example.
  • Line 3 is the chat widget. It isn't an iframe. It's a script tag, so the widget's code runs inside Ravi's own page. async lets the page carry on while it downloads.

web.dev says embeds usually come in two forms: an iframe, or "a script snippet that dynamically injects an <iframe>". Either way, the browser fetches everything the embed needs, as if it were part of your page.

Real-life analogyA stall inside the shop

Ravi lets a bakery run a small stall in a corner of his shop. The stall has its own staff, its own deliveries and its own rules. Ravi decides where it stands and how big it is. He doesn't decide what it orders, or how long its staff block the aisle.

In real life: The stall's corner of the shop
In the page: The embed's box on the page
In real life: The stall's own deliveries
In the page: The embed's own files: its HTML, styles, scripts and pictures
In real life: The stall's staff blocking the aisle
In the page: The embed's scripts busy on the page's main thread
In real life: Where the stall stands, and how big it is
In the page: The parts Ravi controls: where the embed sits, and its size

What real embeds weigh

Lighthouse's docs show a YouTube player of 540 KB, against 3 KB for a facade. web.dev says embeds also "keep the main thread busy when executing". And it warns: "Many popular embeds include over 100 KB of JavaScript, sometimes even going up to 2 MB."

The Third Party Web project adds this up across about 4 million sites. It crawled them with Lighthouse on mobile. On the pages that use YouTube, its scripts ran for 6,297 ms on average.

The course's stand-ins

The course built its own three. Each is sized after those sources.

  • A video player at player.example: a page with a stylesheet, a big script and a poster picture. The poster is the still picture shown before the video starts. The player plays the brewing video when someone presses Play.
  • A map at maps.example: a page with a stylesheet, a big script and twelve map tiles.
  • A chat widget at chat.example: a big script that puts an "Ask Ravi" bubble on the page.

Each one does a fixed amount of made-up work when it starts, the way real ones set themselves up. The amount is the course's choice, and far less than 6 seconds, so read their long tasks as a small example. Each site is a server on the course's own computer, and Chrome was told that all four names lead there.

One more setting matters. On a computer, Chrome runs each site in a process of its own, iframes included. A process is a separate running program, with its own main thread (see Processes and threads). This is called Site Isolation. Not every phone can afford it. Chromium's docs say Android phones with less than 2 GB of memory have no Site Isolation. On bigger ones, only some sites get their own process. So the course turned Site Isolation off, with Chrome's own switch. The player and the map ran in the shop page's process, on its main thread. That's what would happen on such a phone.

What each embed cost

What each embed fetched and ran before any tapPop out in the code editor (opens in a new tab)JavaScript
// Before any tap, on the page with the real embeds, as recorded (the median of 3 loads)const embeds = {  player: { bytes: 482466, requests: 4, longTasksMs: 164 },  map: { bytes: 323151, requests: 15, longTasksMs: 85 },  chat: { bytes: 272910, requests: 3, longTasksMs: 134 },}; for (const [name, { bytes, requests, longTasksMs }] of Object.entries(embeds)) {  console.log(`${name}: ${bytes} bytes, ${requests} requests, ${longTasksMs} ms of long tasks`);}
  • Line 1 is a comment. Lines 3 to 5 hold each embed's bytes, its requests, and its time in long tasks.
  • Line 9 prints a line for each.
  • The first is player: 482466 bytes, 4 requests, 164 ms of long tasks.
  • Then come map: 323151 bytes, 15 requests, 85 ms of long tasks and chat: 272910 bytes, 3 requests, 134 ms of long tasks.
Bytes before any tap, by site. Video player: 482,466 bytes with the real embeds, 4,521 with facades, 164 ms of long tasks. Map: 323,151 bytes with the real embeds, 0 with facades, 85 ms of long tasks. Chat widget: 272,910 bytes with the real embeds, 0 with facades, 134 ms of long tasks. The shop's files: 13,566 bytes with the real embeds, 13,874 with facades, 0 ms of long tasks.Bytes before any tapLong tasksVideo player482,4664,521164 msMap323,151085 msChat widget272,9100134 msThe shop's files13,56613,8740 msreal embedsfacades
Where the bytes came from before Asha tapped anything, and how long each embed's long tasks kept her page busy. With facades, the other sites sent one small thumbnail, and there were no long tasks at all.

Four findings:

  • The map cost 323,151 bytes in 15 requests. It sat far below the screen, and Asha never scrolled to it. An iframe with no loading attribute loads at once, wherever it sits.
  • Every embed ran long tasks on her main thread. A long task holds the main thread for more than 50 ms. While it runs, the page can't answer a tap. The chat widget's came right after its script arrived. The player's and the map's came from inside their iframes, since they shared the page's process. Blocking time adds up the part of each long task over 50 ms. Total Blocking Time counts it the same way (see The supporting metrics). It was 185 ms, from the first paint to the tap.
  • The load event waited for every iframe. The HTML standard says an iframe that isn't lazy loaded "potentially delays the load event". It came at 7,970 ms, against 1,280 ms with facades. Any code the page runs on load waited too.
  • LCP hardly moved. LCP, Largest Contentful Paint, is when the biggest picture or text in the first screen appears. Here it was the mint tea photo on both pages: 1,332 ms with the real embeds, and 1,288 ms with facades. The photo was small, and Chrome asked for it first. On a slower link, web.dev says lazy loading would help. Iframes below the screen would wait, leaving "more bandwidth to load resources that contribute to a page's LCP".

Pick an embed below. You'll see every file it fetched, and each of its long tasks.

Playground: what each embed fetched and ran
Its files
  1. /embed/brew238 bytes, 613 ms to 1,200 ms
  2. /s/player.css26,805 bytes, 1,210 ms to 2,398 ms
  3. /s/player.js430,004 bytes, 1,211 ms to 7,158 ms
  4. /vi/brew/maxres.jpg25,419 bytes, 7,265 ms to 7,974 ms
Its long tasks on the main thread
  1. Task 1, from 7,167 ms99 ms
  2. Task 2, from 7,273 ms65 ms
Try it yourself
4 requests, 482,466 bytes, 2 long tasks

This embed is an iframe: a page of its own, with its own files. Here it shared the shop page's process, so its work ran on the same main thread.

Recorded in Chrome 157, on a phone's screen, with DevTools' Slow 4G and a CPU 4 times slower, with the course's stand-in embeds. From the load with the middle LCP time, before any tap. Times are in ms after Asha opened the page.

Why it matters: an embed costs every visitor its bytes and its work, even the ones who never use it. Count that cost before you paste the snippet.

Facades for video, maps and chat

Lighthouse's docs define the fix. A facade is "a static element that looks similar to the embedded third-party, but is not functional and therefore much less taxing on the page load." It works in three steps.

  • When the page loads: show the facade.
  • When a pointer comes over it: start connecting to the embed's site.
  • When someone clicks it: replace the facade with the real embed.

A facade for the video

Ravi's video facade is a button with the video's thumbnail inside.

The facade: a button with a thumbnailHTML
<button class="video-facade" type="button" aria-label="Play: How to brew mint tea">
  <img src="https://player.example/vi/brew/thumb.jpg" alt="" width="480" height="270">
</button>
  • Line 1 is a real button, so a keyboard or a screen reader can reach it. Its aria-label says what it does.
  • Line 2 is the thumbnail, a small picture of the video, from the video site: 4,521 bytes. Its alt is empty, because the button already has a label.
  • Line 3 closes the button.

The red Play button is drawn with CSS, on top of the thumbnail. This rule gives the facade the same box as the real player.

One box for the facade and the playerCSS
.video, .video-facade { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; border: 0; padding: 0; }

Line 1 makes two things the full width of the page: .video, the real player's iframe, and .video-facade. Both get the same 16 by 9 shape. So when one replaces the other, nothing around them moves. The last section explains aspect-ratio. Then come a few lines of script:

Swapping in the real playerJavaScript
const facade = document.querySelector(".video-facade");facade.addEventListener("pointerover", () => {  const hint = document.createElement("link");  hint.rel = "preconnect";  hint.href = "https://player.example";  document.head.append(hint);}, { once: true });facade.addEventListener("click", () => {  const player = document.createElement("iframe");  player.className = "video";  player.src = "https://player.example/embed/brew?autoplay=1";  player.title = "How to brew mint tea";  player.allow = "autoplay; fullscreen";  facade.replaceWith(player);});
  • Line 1 finds the facade.
  • Lines 2 to 7 run once, the first time a pointer comes over the facade. They add a preconnect hint: a request to open a connection to player.example before it's needed (see Resource hints).
  • Lines 8 to 15 run on a click. Line 9 makes an iframe, and line 10 gives it the player's class, so it gets the same box.
  • Line 11 points it at the player, with ?autoplay=1, so the video starts as soon as the player is ready. Line 12 gives it a title, and line 13 lets it start by itself and go full screen.
  • Line 14 puts the player where the facade was.

What the tap costs

A facade doesn't make the player any lighter. It moves the cost to the moment someone asks for it. The course waited for both pages to settle. Then it tapped Play on each, and timed how long the video took to show its first frame.

From the tap to the videoPop out in the code editor (opens in a new tab)JavaScript
// From the tap on Play to the video's first frame, as recorded (the median of 3 loads)const realPlayer = { waitMs: 837, bytesAfterTap: 65616 };const facade = { waitMs: 4722, bytesAfterTap: 548059 }; console.log(`Real player: ${realPlayer.waitMs} ms, ${realPlayer.bytesAfterTap} bytes after the tap`);console.log(`Facade: ${facade.waitMs} ms, ${facade.bytesAfterTap} bytes after the tap`);
  • Line 1 is a comment.
  • Line 2 is the real player, already loaded: after the tap, it only had to fetch the video.
  • Line 3 is the facade: after the tap, it had to fetch the whole player, then the video.
  • Lines 5 and 6 print Real player: 837 ms, 65616 bytes after the tap and Facade: 4722 ms, 548059 bytes after the tap.
After the tap on Play. Tapping Play on the real player: +56 ms, loading the video; +840 ms, the video. Tapping the facade: +48 ms, a blank box; +1,475 ms, a black box; +3,821 ms, the player; +3,908 ms, loading the video; +4,734 ms, the video.Tapping Play on the real player+56 ms after the tap: loading the video+56 msloading the video+840 ms after the tap: the video+840 msthe videoTapping the facade+48 ms after the tap: a blank box+48 msa blank box+1,475 ms after the tap: a black box+1,475 msa black box+3,821 ms after the tap: the player+3,821 msthe player+3,908 ms after the tap: loading the video+3,908 msloading the video+4,734 ms after the tap: the video+4,734 msthe video
Each frame is Asha's whole screen, timed from her tap. The real player only had to fetch the video. The facade had to fetch the player too, so the video came seconds later.

After tapping the facade, Asha saw a blank box. Then came a black one, then the player, and only then the video. That's the real price of a facade. The people who want the video pay for the player, and they pay later. Everyone else pays almost nothing. The preconnect hint starts the connection a little earlier, while the pointer is over the facade.

Ravi's facade is a small version of lite-youtube-embed, a facade for YouTube. Its README says it "renders just like the real thing but approximately 224× faster." Its code works like Ravi's. It connects early when a pointer comes over it, like line 2. On a click, it adds YouTube's iframe, with autoplay=1. Its comments add a catch: Safari and most phone browsers don't carry the tap through to the new iframe. So on those, it also loads YouTube's player script after the tap, "1KB + 165KB" more.

A facade for the map

A map's facade can be just a picture. web.dev notes that "not every user browsing a restaurant page will click, expand, scroll, and navigate the map embed." It suggests a static image instead. That can be a screenshot, or a picture from a maps service. Here is Ravi's.

A picture of the map, linking to the real oneHTML
<a class="map" href="https://maps.example/place/leaf-and-kettle">  <img src="map-leaf-and-kettle.jpg" alt="Map: Leaf and Kettle, 12 Market Street" width="600" height="450" loading="lazy"></a>
  • Line 1 is a link to the full map on the maps site. It's there for anyone who wants to move the map around.
  • Line 2 is a picture of the map. Its alt says where the shop is. loading="lazy" holds it back until the reader comes near (see Loading images well).
  • Line 3 closes the link.

Before any tap, the picture cost nothing: it was far down, and lazy.

A facade for the chat

web.dev says most chat widgets "download significant amounts of JavaScript". Their facade is a button that looks like the bubble:

A button that loads the chat on a tapHTML
<button class="chat-facade" type="button">Ask Ravi</button><script>document.querySelector(".chat-facade").addEventListener("click", (event) => {  const widget = document.createElement("script");  widget.src = "https://chat.example/widget.js";  widget.dataset.open = "true";  document.body.append(widget);  event.currentTarget.remove();}, { once: true });</script>
  • Line 1 is the button, styled like the bubble.
  • Lines 3 to 9 listen for a click. { once: true }, on line 9, means they run only the first time.
  • Line 4 makes a script tag, and line 5 points it at the real widget. Line 6 tells the widget to open at once, since Asha asked for it.
  • Line 7 adds the script to the page, so it downloads and runs. Line 8 removes the facade.

React Live Chat Loader does this for chat services such as Intercom and Help Scout. web.dev says it swaps in the real widget "when the user hovers or clicks on the button or if the page has been idle for a long time."

Lighthouse and facades

Lighthouse 7 added an audit called "Lazy load third-party resources with facades". It looked for products on a list kept by the Third Party Web project. For each one it found, it showed the transfer size and the blocking time. The list named video players, such as YouTube and Vimeo, and chat widgets, such as Intercom and Drift. Lighthouse 13 removed the audit. Its team wrote that it "covers limited facades and some developers expressed concern using non-affiliated third-party facades. Ultimately, we would prefer the third-parties improve their offerings rather than work around them." Now you count the cost yourself. No tool flags a missing facade.

Compare both versions of the page below, frame by frame, until the video played.

Playground: the real embeds or facades
What Asha saw, until the video played
  1. The mint tea page at 9 ms, The real embeds9 ms
  2. The mint tea page at 673 ms, The real embeds673 ms
  3. The mint tea page at 1,207 ms, The real embeds1,207 ms
  4. The mint tea page at 1,333 ms, The real embeds1,333 ms
  5. The mint tea page at 2,423 ms, The real embeds2,423 ms
  6. The mint tea page at 5,672 ms, The real embeds5,672 ms
  7. The mint tea page at 7,278 ms, The real embeds7,278 ms
  8. The mint tea page at 7,856 ms, The real embeds7,856 ms
  9. The mint tea page at 7,988 ms, The real embeds7,988 ms
  10. The mint tea page at 9,682 ms, The real embeds9,682 ms
  11. The mint tea page at 10,466 ms, The real embeds10,466 ms
  12. The mint tea page at 10,755 ms, The real embeds10,755 ms
  1. Words on the screen (FCP)672 ms
  2. The photo (LCP)1,332 ms
  3. Bytes before any tap1,092,093
  4. From the other three sites1,078,527
  5. Requests before any tap24
  6. Blocking time before the tap185 ms
  7. Load event7,970 ms
  8. From the tap to the video837 ms
  9. Bytes after the tap65,616
Try it yourself
1,092,093 bytes before any tap, then 837 ms from the tap to the video

Everything arrived with the page: the player, the map far down, and the chat widget. When Asha tapped Play, only the video itself was left to fetch.

Recorded in Chrome 157, on a phone's screen, with DevTools' Slow 4G and a CPU 4 times slower, with the course's stand-in embeds. Numbers are medians of 3 loads; the frames come from the load with the middle LCP time, and the tap came at 9,626 ms.

Why it matters: for an embed most visitors never use, a facade saves nearly all its cost. The ones who use it wait longer. Keep facades for embeds that are a choice, not for the thing the page is for.

Lazy-loading iframes

Some embeds aren't a choice. Think of comments, or a social post far down an article. They're there to be read when the reader gets to them. Nobody taps to open them. For those, iframes have an attribute.

The map, lazyHTML
<iframe class="map" src="https://maps.example/embed?place=leaf-and-kettle" title="Map: Leaf and Kettle" width="600" height="450" loading="lazy"></iframe>

Line 1 is Ravi's map, with loading="lazy" at the end. With lazy, the browser waits until the iframe comes near the screen. With no attribute, it loads at once. The HTML standard says the attribute's purpose "is to indicate the policy for loading iframe elements that are outside the viewport." The viewport is the part of the page on screen.

You met the same attribute on images in Loading images well. There, it was like a waiter who sets off just before you sit down. For iframes, Chrome sets off earlier. Chromium 157 keeps a distance for each kind of connection. On 4G, an image loads when it's 1,250 pixels from the screen, and an iframe at 2,500. On 3G, an iframe loads at 3,500. In the recording, Chrome reported a 3G connection under DevTools' Slow 4G.

Real-life analogyPutting the kettle on as a guest comes up the path

Ravi doesn't boil water for everyone who might visit. When he sees a guest at the garden gate, he puts the kettle on. The tea is ready when they sit down. Juice takes no time, so he pours it when they reach the door.

In real life: A guest coming up the path
In HTML: Asha scrolling toward the map
In real life: Putting the kettle on at the gate
In HTML: Starting a lazy iframe 2,500 pixels before it's on screen, on 4G
In real life: Pouring juice at the door
In HTML: Starting a lazy image closer: 1,250 pixels on 4G
In real life: Guests who never come up the path
In HTML: Visitors who never scroll near: the map never downloads

The course loaded a page with only the map, after a long stretch of page. It loaded it with and without the attribute. Then it scrolled to the map.

The map far down, two waysPop out in the code editor (opens in a new tab)JavaScript
// The map, after 5,097 px of page, as recorded (the median of 3 loads)const ways = {  "No loading attribute": { before: 323151, loadEvent: 4254, wait: 0 },  'loading="lazy"': { before: 0, loadEvent: 1250, wait: 3710 },}; for (const [way, { before, loadEvent, wait }] of Object.entries(ways)) {  console.log(`${way}: ${before} bytes before scrolling, load event at ${loadEvent} ms, ${wait} ms wait at the map`);}
  • Line 1 is a comment. Lines 3 and 4 hold each way's bytes before scrolling, its load event, and the wait at the map.
  • Line 8 prints a line for each.
  • The first is No loading attribute: 323151 bytes before scrolling, load event at 4254 ms, 0 ms wait at the map.
  • The second is loading="lazy": 0 bytes before scrolling, load event at 1250 ms, 3710 ms wait at the map.

Without the attribute, Asha paid for the whole map, and the load event waited about 3 seconds more for it. With it, she paid nothing until she scrolled. The map's top sat 4,274 pixels below the bottom of her 823-pixel screen, beyond Chrome's 3,500. The cost moved to the scroll: she waited 3,710 ms at the map.

The standard adds two rules. A lazy iframe that hasn't loaded doesn't hold the load event back. And lazy loading only happens when JavaScript is on. MDN calls this "an anti-tracking measure". Without scripts, a server could follow your scrolling. It would only need to watch which iframes you ask for.

Lazy loading and facades are different tools. Read each row across: what loads with the page, when the real embed loads, and the catch.

Four ways to put an embed on a page
WayWhat loads with the pageWhen the real embed loadsThe catch
The real embedEverything it needs, at once.With the page.Bytes and work for every visitor: 1,078,527 bytes from the three other sites here.
loading="lazy"Nothing, while it's far down.When the visitor scrolls near it.A wait when they get there: 3,710 ms for the map.
A facadeA picture or a button.When the visitor taps it.A wait after the tap: 4,722 ms for the player.
A picture and a linkA picture.Never on this page: the link opens the real one.The visitor leaves the page to use it.

web.dev advises using these techniques "to load only offscreen or non-primary page content." First-screen embeds stay eager. That means they load at once, as they do with no attribute.

Playground: the map far down, two ways
The map
  1. Bytes before scrolling323,151
  2. Load event4,254 ms
  3. From the scroll to the mapno wait: it was ready
  4. Bytes once it was ready323,151
  5. Connection type Chrome reported3g
Your browser
  1. Has loading on iframeschecking…
  2. Your connection typechecking…
  3. Chromium's iframe distance for itchecking…
Try it yourself
323,151 bytes of map before Asha scrolled

The map loaded with the page, though it sat far below the screen. It was ready long before she scrolled to it.

Recorded in Chrome 157, on a phone's screen, with DevTools' Slow 4G and a CPU 4 times slower, with the course's stand-in embeds. The map's top sat 5,097 px down the page; the screen is 823 px tall. Medians of 3 loads.

Why it matters: an embed far down can cost a whole page's worth of bytes before anyone sees it. One attribute moves that cost to the visitors who scroll there.

Reserving space for embeds

Embeds have one more cost, and it isn't bytes. They can push the page around. A layout shift is visible content moving from one frame to the next. Cumulative Layout Shift, CLS, scores the worst burst of them. A score of 0.1 or less is good (see Core Web Vitals).

Why do embeds shift the page? web.dev says embeddable widgets "often aren't aware of how large their contents are before they load." So the sites that offer them don't always keep space for them, "which causes layout shifts when they finally load."

Real-life analogyA coat on a seat

At the cinema, you save a seat for a friend who's running late. If you leave it empty, someone sits in it, and when your friend arrives the whole row shuffles along. Put your coat on it, and your friend just sits down.

In real life: The coat on the seat
In CSS: aspect-ratio: 16 / 9 on the empty box
In real life: Your friend, still on the way
In CSS: The player, still downloading
In real life: Everyone shuffling along when your friend arrives
In CSS: The steps jumping down: a layout shift

Some video sites give a script embed: an empty box, and a script that puts the player in it.

The video site's script embedHTML
<div class="player-embed" data-video="brew"></div>
<script src="https://player.example/embed.js" async></script>
  • Line 1 is the empty box. Its data attribute says which video to show.
  • Line 2 is the video site's script. When it runs, it adds the player's iframe to the box.

The course loaded the mint tea page with this embed. Then it loaded it again, with one extra rule in the page's CSS.

Keeping the player's spaceCSS
.player-embed { aspect-ratio: 16 / 9; }

Line 1 gives the empty box a 16 by 9 shape. aspect-ratio sets the ratio of a box's width to its height. The box is the page's full width, so the browser works out its height before the player arrives.

CLS, two waysPop out in the code editor (opens in a new tab)JavaScript
// The player from the provider's script, as recorded (the median of 3 loads)
const cls = { noSpace: 0.0677, aspectRatio: 0 };

console.log(`No space reserved: CLS ${cls.noSpace}`);
console.log(`aspect-ratio: 16 / 9: CLS ${cls.aspectRatio}`);
  • Line 2 holds each page's CLS.
  • Lines 4 and 5 print No space reserved: CLS 0.0677 and aspect-ratio: 16 / 9: CLS 0.
The steps under the video, before and after the provider's script put the player in. No space reserved: frames at 663 ms and 5,716 ms, CLS 0.0677. aspect-ratio: 16 / 9: frames at 653 ms and 5,700 ms, CLS 0.No space reservedNo space reserved, before the player came, at 663 msbefore, 663 msNo space reserved, after the player came, at 5,716 msafter, 5,716 msCLS 0.0677aspect-ratio: 16 / 9aspect-ratio: 16 / 9, before the player came, at 653 msbefore, 653 msaspect-ratio: 16 / 9, after the player came, at 5,700 msafter, 5,700 msCLS 0
With no space reserved, the steps sat right under "How to brew", then jumped down when the player came. With aspect-ratio, the box was there from the first paint, and nothing moved.

Without the rule, the steps sat right under "How to brew" for about half a second. At about 1.2 seconds, the script put the player in, and they jumped down. That one shift scored 0.0677: two thirds of the 0.1 that still counts as good, from one embed. With the rule, the box was there from the first paint, and nothing moved.

Why an iframe's width and height aren't enough

An image with width and height keeps its shape, even when CSS makes it 100% wide. The browser turns the two numbers into an aspect ratio. The HTML standard does that for img and video, but not for iframe. On an iframe, they only set its width and its height.

The course checked it with an iframe of width="560" and height="315", styled width: 100%; height: auto. It lost its shape: Chrome gave it a default height, the same as for any iframe. With aspect-ratio: 16 / 9 added, it was 231.75 pixels tall on a screen 412 pixels wide: exactly 16 by 9.

That's why Ravi's CSS gives the player and its facade an aspect ratio. They share one box, so the tap moves nothing. Before the tap, the facade page's CLS was 0. A shift just after a tap wouldn't count anyway. web.dev says shifts "within 500 milliseconds of user input" are flagged, so they can be left out.

Playground: the player's space, two ways
What Asha saw
  1. The page at 11 ms, No space reserved11 ms
  2. The page at 663 ms, No space reserved663 ms
  3. The page at 1,201 ms, No space reserved1,201 ms
  4. The page at 1,261 ms, No space reserved1,261 ms
  5. The page at 2,651 ms, No space reserved2,651 ms
  6. The page at 5,019 ms, No space reserved5,019 ms
  7. The page at 5,590 ms, No space reserved5,590 ms
  8. The page at 5,716 ms, No space reserved5,716 ms
  1. CLS0.0677
  2. The script put the player inat 1,198 ms
  3. Layout shiftsone, at 1,194 ms, scoring 0.0677
  4. What moved<ol> and <p>
Try it yourself
CLS 0.0677

The box was empty until the provider's script put the player in. Then everything under it moved down.

Recorded in Chrome 157, on a phone's screen, with DevTools' Slow 4G and a CPU 4 times slower, with the course's stand-in embeds. CLS is the median of 3 loads; the frames come from the load with the middle CLS.

Why it matters: an embed that arrives late should arrive into a space that's already there. One line of CSS keeps the page still.

Pick a loading plan for each embed

For each embed, ask two questions. Will most visitors use it? And does it need a tap to start, or is it read on sight?

Which loading plan fits each embed?
  • A chat bubble on every page of the shop
  • A how-to video halfway down a product page
  • A podcast player in the middle of a long article
  • Comments from a comment service under an article
  • An embedded social post near the end of a blog post
  • A video page whose only content is the video, at the top
  • A card form from the payment provider, on the checkout page
Try it yourself
0 of 7 correct

Sort each embed into the plan it should use.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "An embed only costs something when the visitor uses it." No. Before any tap, Asha downloaded 1,078,527 bytes from the three other sites.
  • "An iframe can't slow down my page's main thread." No. Without Site Isolation, as on some phones, the player's and the map's long tasks ran on Asha's main thread.
  • "A facade makes the video start sooner." No. After the tap, the facade took 4,722 ms to the first frame, and the real player 837 ms. The facade saves the visitors who never tap.
  • "loading="lazy" is the same as a facade." No. A lazy iframe loads when the visitor scrolls near it, whether they want it or not. A facade waits for a tap.
  • "An iframe's width and height attributes keep its shape." No. On an iframe, they don't become an aspect ratio. With width: 100% and height: auto, the shape is lost. Use aspect-ratio.
  • "Lighthouse will tell me which embeds need a facade." No. Lighthouse 13 removed that audit.

Practice exercises

Answer each one first, then open the hints if you need them.

Exercise 1 · Warm-upHow much less

What does the last line print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
const embeds = 1092093;
const facades = 18395;

console.log(`${Math.round((1 - facades / embeds) * 100)}% fewer bytes`);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upDoes the map wait?

    Ravi moves the lazy map up, so its top sits 3,000 pixels down the page. On a 3G connection, what does the last line print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const margins = { "4g": 2500, "3g": 3500 };
    const screen = 823;
    const mapTop = 3000;
    
    const waits = mapTop - screen > margins["3g"];
    console.log(waits ? "The map waits" : "The map loads at once");

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeThe chat's long task

      In the recorded load, when did the chat widget's long task start, in ms after Asha opened the page?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFind the bug in the facade

        This facade works, but after a tap the page looks wrong. Which line has the bug?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const facade = document.querySelector(".video-facade");
        facade.addEventListener("click", () => {
          const player = document.createElement("iframe");
          player.src = "https://player.example/embed/brew?autoplay=1";
          player.title = "How to brew mint tea";
          facade.append(player);
        });

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeAn iframe's default height

          An iframe has width="560" and height="315", and CSS sets width: 100%; height: auto. How tall does Chrome make it, in CSS pixels?

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeHow small a facade should be

            The Third Party Web project lists facades for Lighthouse. As a rough guide, how small should a facade be, compared with the product it imitates?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask what loaded before Asha touched anything, and what she paid for when she did.

              Embeds and facades quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does this load event code print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const eager = 4254;
                const lazy = 1250;
                
                console.log(`${eager - lazy} ms sooner`);

                Choose an answer to see the explanation.

              2. Question 2 of 7In the recording, why did the page with the real embeds fire its load event so much later?

                Choose an answer to see the explanation.

              3. Question 3 of 7What does a facade cost the visitor who taps Play?

                Choose an answer to see the explanation.

              4. Question 4 of 7Where did the chat widget's script run?

                Choose an answer to see the explanation.

              5. Question 5 of 7Why didn't the width and height attributes keep the responsive iframe's shape?

                Choose an answer to see the explanation.

              6. Question 6 of 7Which embed suits loading="lazy" better than a facade?

                Choose an answer to see the explanation.

              7. Question 7 of 7In the recording, which element was the LCP on both versions of the page?

                Choose an answer to see the explanation.

              Key takeaways

              • An embed brings its own files and its own work. Every visitor pays for them, used or not.
              • A script embed runs on your page's main thread. An iframe can too, on phones without Site Isolation.
              • An iframe that isn't lazy loaded holds the load event back.
              • A facade looks like the embed and loads the real one on a tap. The visitors who tap wait longer; the rest save almost everything.
              • loading="lazy" holds back iframes far down until the visitor scrolls near. Keep the first screen eager.
              • Reserve each embed's space with aspect-ratio. An iframe's width and height attributes don't keep its shape.

              Remember the one-liner.
              An embed is another site inside yours: show a look-alike until someone asks, lazy load the far ones, and keep their space ready.

              Sources

              The facts on this page come from these standards, docs and source code:

              Coming next: How web fonts load. Fonts are found late and can hold text back. You'll follow one font from its CSS rule to the painted text.

              CompleteFrontend Clear concepts. Working examples.