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.
- 01Count what an embed costsRead the bytes, requests and main-thread work an embed brings before anyone touches it.
- 02Build 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.
- 03Lazy 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.
1// Asha's phone, before she tapped anything, as recorded (the median of 3 loads)2const embeds = { bytes: 1092093, requests: 24, blockingMs: 185, loadEventMs: 7970 };3const facades = { bytes: 18395, requests: 3, blockingMs: 0, loadEventMs: 1280 };4 5console.log(`Real embeds: ${embeds.bytes} bytes in ${embeds.requests} requests, ${embeds.blockingMs} ms of blocking`);6console.log(`Facades: ${facades.bytes} bytes in ${facades.requests} requests, ${facades.blockingMs} ms of blocking`);7console.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:
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.
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
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:
<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, andtitlenames 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.
asynclets 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.
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
1// Before any tap, on the page with the real embeds, as recorded (the median of 3 loads)2const embeds = {3 player: { bytes: 482466, requests: 4, longTasksMs: 164 },4 map: { bytes: 323151, requests: 15, longTasksMs: 85 },5 chat: { bytes: 272910, requests: 3, longTasksMs: 134 },6};7 8for (const [name, { bytes, requests, longTasksMs }] of Object.entries(embeds)) {9 console.log(`${name}: ${bytes} bytes, ${requests} requests, ${longTasksMs} ms of long tasks`);10}- 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 tasksandchat: 272910 bytes, 3 requests, 134 ms of long tasks.
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
loadingattribute 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.
- /embed/brew238 bytes, 613 ms to 1,200 ms
- /s/player.css26,805 bytes, 1,210 ms to 2,398 ms
- /s/player.js430,004 bytes, 1,211 ms to 7,158 ms
- /vi/brew/maxres.jpg25,419 bytes, 7,265 ms to 7,974 ms
- Task 1, from 7,167 ms99 ms
- Task 2, from 7,273 ms65 ms
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.
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.
<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-labelsays what it does. - Line 2 is the thumbnail, a small picture of the video, from the video site: 4,521 bytes. Its
altis 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.
.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:
1const facade = document.querySelector(".video-facade");2facade.addEventListener("pointerover", () => {3 const hint = document.createElement("link");4 hint.rel = "preconnect";5 hint.href = "https://player.example";6 document.head.append(hint);7}, { once: true });8facade.addEventListener("click", () => {9 const player = document.createElement("iframe");10 player.className = "video";11 player.src = "https://player.example/embed/brew?autoplay=1";12 player.title = "How to brew mint tea";13 player.allow = "autoplay; fullscreen";14 facade.replaceWith(player);15});- 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.examplebefore 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.
1// From the tap on Play to the video's first frame, as recorded (the median of 3 loads)2const realPlayer = { waitMs: 837, bytesAfterTap: 65616 };3const facade = { waitMs: 4722, bytesAfterTap: 548059 };4 5console.log(`Real player: ${realPlayer.waitMs} ms, ${realPlayer.bytesAfterTap} bytes after the tap`);6console.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 tapandFacade: 4722 ms, 548059 bytes after the tap.
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.
1<a class="map" href="https://maps.example/place/leaf-and-kettle">2 <img src="map-leaf-and-kettle.jpg" alt="Map: Leaf and Kettle, 12 Market Street" width="600" height="450" loading="lazy">3</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
altsays 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:
1<button class="chat-facade" type="button">Ask Ravi</button>2<script>3document.querySelector(".chat-facade").addEventListener("click", (event) => {4 const widget = document.createElement("script");5 widget.src = "https://chat.example/widget.js";6 widget.dataset.open = "true";7 document.body.append(widget);8 event.currentTarget.remove();9}, { once: true });10</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.
9 ms
673 ms
1,207 ms
1,333 ms
2,423 ms
5,672 ms
7,278 ms
7,856 ms
7,988 ms
9,682 ms
10,466 ms
10,755 ms
- Words on the screen (FCP)672 ms
- The photo (LCP)1,332 ms
- Bytes before any tap1,092,093
- From the other three sites1,078,527
- Requests before any tap24
- Blocking time before the tap185 ms
- Load event7,970 ms
- From the tap to the video837 ms
- Bytes after the tap65,616
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.
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.
<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.
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.
1// The map, after 5,097 px of page, as recorded (the median of 3 loads)2const ways = {3 "No loading attribute": { before: 323151, loadEvent: 4254, wait: 0 },4 'loading="lazy"': { before: 0, loadEvent: 1250, wait: 3710 },5};6 7for (const [way, { before, loadEvent, wait }] of Object.entries(ways)) {8 console.log(`${way}: ${before} bytes before scrolling, load event at ${loadEvent} ms, ${wait} ms wait at the map`);9}- 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.
| Way | What loads with the page | When the real embed loads | The catch |
|---|---|---|---|
| The real embed | Everything 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 facade | A picture or a button. | When the visitor taps it. | A wait after the tap: 4,722 ms for the player. |
| A picture and a link | A 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.
- Bytes before scrolling323,151
- Load event4,254 ms
- From the scroll to the mapno wait: it was ready
- Bytes once it was ready323,151
- Connection type Chrome reported3g
- Has loading on iframeschecking…
- Your connection typechecking…
- Chromium's iframe distance for itchecking…
The map loaded with the page, though it sat far below the screen. It was ready long before she scrolled to it.
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."
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 / 9on 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.
<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.
.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.
// 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.0677andaspect-ratio: 16 / 9: CLS 0.
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.
11 ms
663 ms
1,201 ms
1,261 ms
2,651 ms
5,019 ms
5,590 ms
5,716 ms
- CLS0.0677
- The script put the player inat 1,198 ms
- Layout shiftsone, at 1,194 ms, scoring 0.0677
- What moved<ol> and <p>
The box was empty until the provider's script put the player in. Then everything under it moved down.
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?
- 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
Sort each embed into the plan it should use.
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%andheight: auto, the shape is lost. Useaspect-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.
What does the last line print?
const embeds = 1092093;
const facades = 18395;
console.log(`${Math.round((1 - facades / embeds) * 100)}% fewer bytes`);98% fewer bytes. 1 minus 0.017 is 0.983, which rounds to 98%. Almost everything the page downloaded came from embeds Asha never used.
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?
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");The map loads at once. 2,177 pixels is inside Chrome's 3,500-pixel distance on 3G, so a lazy map this close starts loading with the page.
In the recorded load, when did the chat widget's long task start, in ms after Asha opened the page?
4,729 ms, right after widget.js finished downloading at 4,725 ms. The widget's code ran as soon as it arrived, on the page's main thread.
This facade works, but after a tap the page looks wrong. Which line has the bug?
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);
});facade.replaceWith(player);Line 6. append puts the player inside the button, under the thumbnail, so the box grows and the thumbnail stays. replaceWith puts the player where the facade was.
An iframe has width="560" and height="315", and CSS sets width: 100%; height: auto. How tall does Chrome make it, in CSS pixels?
150. With height: auto and no aspect ratio, Chrome falls back to an iframe's default height, the same 150 pixels MDN lists for an iframe with no height at all. aspect-ratio: 16 / 9 fixes it.
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?
Under 10%. The page says a facade should be less than 10% of the product it imitates. Ravi's video facade, 4,521 bytes of thumbnail, is far below that.
Check your understanding
For each question, ask what loaded before Asha touched anything, and what she paid for when she did.
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)JavaScriptconst eager = 4254; const lazy = 1250; console.log(`${eager - lazy} ms sooner`);Choose an answer to see the explanation.
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.
Question 3 of 7What does a facade cost the visitor who taps Play?
Choose an answer to see the explanation.
Question 4 of 7Where did the chat widget's script run?
Choose an answer to see the explanation.
Question 5 of 7Why didn't the width and height attributes keep the responsive iframe's shape?
Choose an answer to see the explanation.
Question 6 of 7Which embed suits loading="lazy" better than a facade?
Choose an answer to see the explanation.
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:
- Best practices for using third-party embeds, web.dev.
- Lazy load third-party resources with facades, Chrome for Developers.
- third-party-web, the README: methodology and average impact, Patrick Hulce and contributors.
- Process Model and Site Isolation, the Chromium project.
- The disable-site-isolation-trials switch, Chromium 157, the Chromium project.
- The iframe element, WHATWG, the HTML Standard.
- Lazy load images and <iframe> elements, web.dev.
- lite-youtube-embed 0.3.4, the README, Paul Irish.
- lite-youtube-embed 0.3.4, lite-yt-embed.js, Paul Irish.
- third-party-web: facades, Patrick Hulce and contributors.
- Lighthouse 12.8.2: the third-party-facades audit, the Lighthouse project.
- What's new in Lighthouse 13, Chrome for Developers.
- It's time to lazy-load offscreen iframes!, web.dev.
- Lazy loading attributes, WHATWG, the HTML Standard.
- <iframe>: the Inline Frame element, MDN.
- Lazy loading distances for frames and images, Chromium 157, the Chromium project.
- Largest Contentful Paint (LCP), web.dev.
- Cumulative Layout Shift (CLS), web.dev.
- Optimize Cumulative Layout Shift, web.dev.
- Rendering: attributes for embedded content and images, WHATWG, the HTML Standard.
- aspect-ratio, MDN.
- Chrome DevTools' network presets (NetworkManager.ts), the Chrome DevTools project.
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.