Zach’s ugly mug (his face)

Zach Leatherman

Ruthlessly Eliminating Layout Shift on

November 25, 2020

On the Netlify web site, we have a little banner that appears at the top to drive traffic to new and exciting things happening in Netlify-land.

Screenshot of the banner on

That banner has exactly two features:

  1. An advanced HTML feature known only to a select few Old Guard developers: the hyperlink.
  2. A close button (which saves the preference for future page loads)

There are a few key performance milestones in the lifecycle of this component, and this is how worked previously:

  1. The page’s initial render. The banner is ⚠️⚠️⚠️ hidden by default. Without JavaScript or before JavaScript loads, the banner is hidden.
  2. After the JavaScript loads, we check localStorage to see if the user has closed the banner previously. We hash this preference to the banner URL so that if the banner changes, it will render even if the user has opted out previously. If applicable, render the banner.
  3. Lastly we bind JavaScript events to the close button. Events are not necessary for the hyperlink, because its behavior is delivered exlusively in HTML (very much wow).

Steps 2 and 3 were bundled and executed together in the same component code file. And in some earlier iterations of the site, up the amount of time that elapsed between Step 1 and 2 could be up to ~600 ms.

Filmstrip showing hidden banner for ~600ms on old design

On our new site (faster, mind you) redesign we inlined the JavaScript for Steps 2 and 3 into the end of the <body> and the delay was still very present:

Filmstrip showing hidden banner for ~600ms on the new design

The fix

What we needed to do was swap the behavior. The common use case was to visit the site without the opt-in preference to hide the banner. We must make the banner visible by default and make the JavaScript path to hide it the exception to the rule.

This changes our previously mentioned Step 1, the page’s initial render, show the banner. Without JavaScript or before JavaScript loads, the banner should be visible.

We also split the JavaScript code for the component into two separate pieces: one piece to check whether or not the user has the preference to hide and a separate Web Component to bind the events.

Update: I’ve packaged up the code below and put it on GitHub for re-use.


We use opacity to toggle the close button so that it doesn’t reflow the component when it’s enabled via JavaScript.

.banner--hide announcement-banner,
display: none;
[data-banner-close] {
opacity: 0;
pointer-events: none;
.banner--show-close [data-banner-close] {
opacity: 1;
pointer-events: auto;
<a href="">Read about our Sustainability</a>
<button type="button" data-banner-close>Close</button>


banner-helper.js, put into the <head>:

// the current banner CTA URL, we inject this from a JSON data file
let ctaUrl = "";
let savedCtaUrl = localStorage.getItem("banner--cta-url");

if(savedCtaUrl === ctaUrl) {

banner.js, defer this until later (how much later is up to you):

class Banner extends HTMLElement {
connectedCallback() {
// No matter when this runs, the close button will not be visible
// until after this class is added—prevents ghost clicks on the button
// before the event listener is added.

let button = this.getButton();
if(button) {
button.addEventListener("click", () => {

getButton() {
return this.querySelector("[data-banner-close]");

savePreference() {
let cta = this.querySelector("a[href]");
if(cta) {
let ctaUrl = cta.getAttribute("href");
localStorage.setItem("banner--cta-url", ctaUrl);

close() {
this.setAttribute("hidden", true);

window.customElements.define("announcement-banner", Banner);

Astute readers will notice that the above is a web component but let’s just keep that between us.

The Results

Note that the first render contains the banner! This is the same render behavior whether or not JavaScript is in play.

Filmstrip showing banner visible on first render

In this waterfall comparison, you might note that we reduced layout shift metrics to zero.

Graph of Layout Shifts: previous has .35 and new has 0

And because we inlined the script for repeat views into the <head>, when you hide the banner and navigate to a new page, the banner will be hidden before first render too.

Not too bad for a few small changes!

Next target will be to improve the web font render.

Zach’s ugly mug (his face)

Zach is a builder for the web with Netlify. He created the Eleventy static site generator and is still fixated on web fonts. His public speaking résumé includes talks in eight different countries at events like Beyond Tellerrand, Smashing Conference, CSSConf, and The White House. He is an emeritus of Filament Group, NEJS CONF, and still helps out with NebraskaJS. Read more about Zach »

Images at Jamstack Toronto
SmashingConf Austin 2020


WestbrookNaveenBruce AndersonAlan DávalosBryce WraySami KeijonenAndy WeisnerJustin FagnaniDion AlmaerDuncan 🌲 Taproot ⇛ BIP 8Jeremy WagnerMatt BiilmannComandeerAndy Bell
Eduardo BouçasWestbrookCassey LottmanJay Hoffmanndies das 🦂 contentVladislav ShkodinSamar PandaVilleRyan StanleyRob BlakeFrank NoirotPaul GrenierThomas A. PowellLars den BakkerMasataka YakuraReto Ryter 🏠🧟‍♂️Bruce AndersonRamonaRyan CaohenningParkerBond usePrivilege(stopRacism)Lasha KrikheliKiran SonleyTrung NguyenKatie Sylor-Millervínαч puppαltylerPeter DemariaShingo YamazakiAhmad Saleemaaron hansAlan DávalosbertrandkellerTanner DolbyAnnamalaiKristof NeirynckRico 🙇🏻‍♂️Bryce WrayAdam Di MarioRhian van EschKyle HallAmelia Bellamy-RoydsAndy BellMonica🌳 Emanuel Kluge 🐿Matt BiilmannJohn LiuSaddam M. 🇸🇴Maxime RichardTim van der Lippeandrew levineAjay PoshakDana ByerlyJared WhiteBrett EvansMatthias OttPhil WolstenholmeArjun SajeevJosh ThomasJelmer de MaatMarcus 🥦 🚴‍♀️Ashur Cabreraemmett naughtontanangularDavid ZeledonBrad FrostEduardo UribeFlorian GeierstangerJacky HuHeather BuchelChris JacksonMatt StrömDion AlmaerEric ☕️ 🐟 💻Justin FagnaniKenneth Auchenberg 💭Juan FernandesDaniel SchildtRobin RendleberliAndy WeisnerMatt MulderTodd MoreySamuel HauserDuncan 🌲 Taproot ⇛ BIP 8Nikita VoloboevRob SterliniRiccardo ErraBenJack FranklinAndy BellChirag JainNicholas C. ZakasMatt Wing 🦃Yildirim Karal 💻+💡+❤️=🚀Andy DaviesDerekGabriel Calin LazarNathan SmithKristóf Poduszló 🦄Mike SmedleyJohn MeyerhoferEric WallaceJoel G GoodmanTRST_BlogBrett Jankord
24 Replies
  1. I do so regularly, so yes.

  2. Zach Leatherman

    Zach Leatherman @zachleat #

    cAN i CUrSe toO or aRe the bAd WORdS oFF lIMITs

  3. We should be sharing the good word of webcomponents. Allllll the good words.... O_O

  4. Stoyan Stefanov

    Stoyan Stefanov @stoyanstefanov #

    haha, I think I got it! My 500 char substring cut off the ⚠️ emoji so json_encode couldn't handle it…

  5. Zach Leatherman

    Zach Leatherman @zachleat #

    😅 sORrY!

  6. Stoyan Stefanov

    Stoyan Stefanov @stoyanstefanov #

    HM, and youR fEed BrOKe PErfPLaNEt.cOM LOokINg... :)

  7. Šime Vidas

    Šime Vidas @simevidas #

    Basically, splitting the page into parts and loading, executing, and rendering each part separately in sequential order.

  8. Zach Leatherman

    Zach Leatherman @zachleat #

    Is this separate from the normal progressive rendering behavior of HTML

  9. Šime Vidas

    Šime Vidas @simevidas #

    This sounds like a use-case for progressive rendering.

  10. Zach Leatherman

    Zach Leatherman @zachleat #

    The other complaint I currently have about our banner is that the small viewport nav close button isn’t in the same place as the open hamburger icon when the banner is open 😱

  11. Zach Leatherman

    Zach Leatherman @zachleat #

    ❤️ I think it’s a fair point! Just for the technical discussion about hiding the banner on the target page, I’m also thinking about the benefit of navigation stability on navigation between pages.

  12. Lukas Grebe

    Lukas Grebe @LukasGrebe #

    I ❤️ netlify and marketing IS marketing 🤷‍♂️ So let’s just leave it at „yay for less content shifting and a faster web“ 🥰

  13. Lukas Grebe

    Lukas Grebe @LukasGrebe #

    FTA:“The common use case was to visit the site without the opt-in preference to hide the banner. We must make the banner visible by default“ - I’m saying this premise might not be complete

  14. Lukas Grebe

    Lukas Grebe @LukasGrebe #

    Absolutely and this is only a tangent to the technical discussion. (Which w/o Marketing wouldn’t be needed) Im saying a general banner for everyone on every page (even the landing page of the banner) might not be the best way to go.

  15. Søren Birkemeyer 🦊

    Søren Birkemeyer 🦊 @polarbirke #

    Because marketing.

  16. Lukas Grebe

    Lukas Grebe @LukasGrebe #

    Look: German has a name for itörer_(W… Still. I wonder if the very first impression shouldn’t be a Störer but something else.

  17. Zach Leatherman

    Zach Leatherman @zachleat #

    Yeah I’d wager that too 👍🏻

  18. Lukas Grebe

    Lukas Grebe @LukasGrebe #

    a fun anecdote: I heard you like banners so we put a banner on the landing page of the banner so you can click the banner to see another banner!

  19. Lukas Grebe

    Lukas Grebe @LukasGrebe #

    I don’t necessarily understand banners. I’ve navigated to a specific content for some reason. Why distract and point my attention at some other very specific content, instead of maybe pointing out this content AFTER the content I originally came for?

  20. Jouni Kantola

    Jouni Kantola @jouni_kantola #

    All right! Thanks for the writeup. By that, I gather head is the way to go for light/dark mode toggles as well.

  21. Zach Leatherman

    Zach Leatherman @zachleat #

    Yeah, that’s too late. The second filmstrip in the post showed similar behavior

  22. Jouni Kantola

    Jouni Kantola @jouni_kantola #

    Would there have been any performance upside or downside by running the class toggle script in the web component instead to not have blocking code in head?

  23. Zach Leatherman

    Zach Leatherman @zachleat #

    Awesome! 👍🏻

  24. Phil Wolstenholme

    Phil Wolstenholme @philw_ #

    Thanks for writing this up, I've just changed my approach on a project I'm working on now as a result of seeing the way you're using JS (but in the `head`) to control visibility 👍

    15 Mentions
    1. #

    2. #

    3. Jens Tangermann

      Jens Tangermann @J3nsT #

      Ruthlessly Eliminating Layout Shift on—…

    4. Netlify

      Netlify @Netlify #

      "Ruthlessly eliminating layout shift on" — Meticulous detail and valuable lessons from @zachleat…

    5. Stefan Böck

      Stefan Böck @stefanboeckname #…

    6. Jeremias Menichelli

      Jeremias Menichelli @jeremenichelli #

      Reading this good piece by @zachleat got me thinking two things, first how important inling critical stuff still is. Second, if a resource doesn't prevent Layout Shift, there's a high chance it could be deferred or lazy loaded…

    7. WebPerf / WPO Tips

      WebPerf / WPO Tips @WPOtips #

      📑 Interesting blog post by @zachleat: Ruthlessly Eliminating Layout Shift on netlify​.com:… #webperf #css

    8. Planet Performance

      Planet Performance @perfplanet #

      "Ruthlessly Eliminating Layout Shift on" by @zachleat…

    9. Alexander Boldakov

      Alexander Boldakov @boldakov #

      Ruthlessly Eliminating Layout Shift on—…

    10. Tim van der Lippe

      Tim van der Lippe @TimvdLippe #

      "Astute readers will notice that the above is a web component but let’s just keep that between us." Choose a tool that helps you do the job. Glad to see web components being useful to increase the user experience and reduce user frustration with no jumping elements.

    11. Michael Scharnagl

      Michael Scharnagl @justmarkup #

      An advanced HTML feature known only to a select few Old Guard developers: the hyperlink. 😂

    12. Saddam M. 🇸🇴

      Saddam M. 🇸🇴 @undefinedbuddy #

      Amazing work!

    13. Josh Thomas

      Josh Thomas @jthoms1 #

      Awesome! Now use web components for your design systems. 🙌

    14. Brian LeRoux

      Brian LeRoux @brianleroux #

      Web components are excellent for progressive enhancement. Love to see it.

    15. Brian LeRoux

      Brian LeRoux @brianleroux #

      Web components are excellent for progressive enhancement. Love to see it.

    Social Card Image Preview

    This is what will show up when you share this post on Social Media:

    How did you do this? I automated my Open Graph images. (Peer behind the curtain at the test page)