Have you noticed sometimes after a website loads, elements can pop in and push everything else around? This pet peeve of mine is known as “jank”, and it annoys the hell out of me.
Even Drupal’s Admin user interface sometimes does this! Usually that’s because we make a best effort to support non-JavaScript environments. The browser will do its initial paint as soon as the stylesheets are loaded. After that the JS is parsed and executed and makes changes to the DOM, like adding CSS classes, moving around elements, and injecting elements. Each change has the potential to cause jank.
This has bugged the ever-loving 💩 out of me for a while. Drupal is an amazing content management system, and it needs to feel like that for people that might be evaluating it for the first time.
In this article, I’ll walk through the root causes, and how me (and several others) are working to fix them in a way that’s resilient, robust, and utilizes some fancy new web technologies.
Some root causes
The trick to eliminate the jank depends on what is causing it. Most (but not all) issues are caused by several root causes:
- Lazy loading of an element (as is often the case with BigPipe). The fix for this is to reserve the space so when it does appear, it doesn’t shift or push down the rest of the page
- JavaScript hiding an element. This happens with the States API, the
js-hide
CSS class, and in a couple other places. The fix is for this to ensure that the element is hidden initially by CSS (unless JavaScript is disabled). - JavaScript moving an element. This happens in the Views UI, and a few other spots. The elements aren’t styled properly (and eat up a lot of space) until they are moved.
Issues and solutions
Using <noscript> to eliminate jank within the dropbutton
I started working on this about a year ago with the dropbutton component first (https://www.drupal.org/i/3361315), because they appear just about everywhere within the admin interface.
In this case the dropbutton loads showing all dropdown options (and eats up vertical space). Once JavaScript kicks in, it adds a js
class to the html
element, which then tells CSS to hide all of the dropdown options.
The solution is to not wait on the JS to trigger the hiding of the dropbutton options. But, since we still want to display them when there’s no JavaScript, we need to be careful how we do it.
In this case, we handled it by injecting a <noscript>
element into the HTML head element, and loading a CSS file within that which has rules to display the dropbutton options. Because the CSS is within the <noscript>
, it’ll only load when there’s no JavaScript available! This was fixed about a year ago, and has already made its way into the latest Drupal versions.
I’m not adding example code in this blog post because we found a better way to do it (see below). But, if you want to check it out, the issue is at https://www.drupal.org/i/3361315.
Using the new CSS media scripting features to eliminate jank is much simpler!
In 2023 browsers released the CSS media scripting feature that promised to negate the need for a <noscript>
tag, while making the code dramatically more simple! I used this first on https://www.drupal.org/i/3404218, where I modified the core .js-hide
and .js-show
CSS classes to use this. In this issue we specifically were concerned about the table filter component (which uses JS to search the page on the modules and permissions pages), but the fix will apply anywhere these classes are used.
In the CSS below, you can find the @media (scripting: enabled)
media query near the bottom. This tells the browser that if JavaScript is available, then show the .js-show
CSS, and hide the .js-hide
class. It does this without waiting for the JS to be executed!
/**
* For anything you want to hide on page load when JS is enabled, so
* that you can use the JS to control visibility and avoid flicker.
*/
.js .js-hide {
display: none;
}
/**
* For anything you want to show on page load only when JS is enabled.
*/
.js-show {
display: none;
}
.js .js-show {
display: block;
}
/**
* Use the scripting media features for modern browsers to reduce layout shifts.
*/
@media (scripting: enabled) {
/* Extra specificity to override previous selector. */
.js-hide.js-hide {
display: none;
}
.js-show {
display: block;
}
}
Fixing jank created by BigPipe lazy-loading content
BigPipe is a core module that was introduced into the standard profile in Drupal 8.5. It improves performance by outputting the initial page to the browser, and then lazy-loading elements that require additional processing and time.
It’s a great solution, but when items are placed onto the page, it can result in some significant jank. Fortunately, a solution was added to core in 10.1, which allows developers to create “placeholder content” that can reserve the space.
I started working on this with the local actions block. The first step is to identify the BigPipe placeholder, which I did by setting a breakpoint at the very beginning of drupal.js, and ensuring that I had Twig template debugging enabled.
From here, I duplicated sample content (which is basically a button), and set it to visibility: hidden
. This tells the browser to reserve the space, but not paint the elements. So, when BigPipe actually injects the correct content, the space will already exist, without the need to push anything else around!
Core release manager Nate Catchpole, ended up fixing this a different way that would apply to all themes (not just Claro), but the template method is still very valid. You can see the issue at https://www.drupal.org/i/3441137
Using the new CSS media feature with a fallback in the event that JavaScript loads… but breaks
What will happen to content hidden via @media (scripting: enabled)
if JavaScript is enabled… but breaks? The answer is that the content remains hidden. The scripting media feature isn’t aware of anything breaking. How do we handle this?
This was the problem we faced with the Views UI action buttons being moved around by JavaScript. We want to hide them in the initial placement, but if the JavaScript breaks and never moves them, we want them to show.
We handled this with a combination of @media (scripting: enabled)
and CSS animations. The media feature enables the animation, which starts in a display: none
state. Then after a 5 second animation-delay
, it will “animate” to display: unset
. The end result of this is a failsafe where the action buttons are hidden for 5 seconds, while it waits for JavaScript to move them to their permanent destination. If that doesn’t happen, they’ll then appear.
/* JS moves Views action buttons under a secondary tabs container, which causes
a large layout shift. We mitigate this by using animations to temporarily hide
the buttons, but they will appear after a set amount of time just in case the JS
is loaded but does not properly run. */
@media (scripting: enabled) {
.views-tabs__action-list-button:not(.views-tabs--secondary *) {
animation-name: appear;
animation-duration: 0.1s;
/* Buttons will be hidden for the amount of time in the animation-delay if
not moved. Note this is the approximate time to download the views
aggregate CSS with slow 3G. */
animation-delay: 5s;
animation-iteration-count: 1;
animation-fill-mode: backwards;
}
}
@keyframes appear {
from {
display: none;
}
to {
display: unset;
}
}
This fix will be included in Drupal 10.3 (due to be released in June). You can see the code in the issue at https://www.drupal.org/i/3441124.
Conclusion and DrupalCon plans!
It’s fun to knock down these pet-peeve issues of mine one by one! If anyone wants to help, I have a meta issue at https://www.drupal.org/i/3404214. There are a number of issues remaining… some of them are easier than others. I’m also giving thoughts on how to abstract some of this logic out, so it’s easier to implement.
I’ll also be at DrupalCon Portland next week, and I’ll be representing Agileana, as I give a talk (with my amazing coworker Ivan) at the Government Summit on Thursday about Gutenberg and Single Directory Components.
I’ll be sprinting on reducing jank, as well as many other things (including the new badass Navigation module) in the contrib room throughout the whole ‘con.
Thank you’s
Super thanks to everyone who helped out on these, including Catch, Lauri, Stephen Mustgrave (who’s everywhere!), and Théodore Biadala (who encouraged me to write this blog)!
Hey you! Leave a comment!3
Seriously... I really like it when people let me know their thoughts and that they've read this.
hi! thanks for taking the time to do this writeup.
don't forget to link to the badass Navigation module at: https://www.drupal.org/project/navigation
cheers
I haven't heard of the scripting media query, very cool.
When i first read how you used it with .js-hide / .js-show, i was yelling at my phone, "But when does JavaScript NOT break? You've hidden all the controls!" My blood pressure had to wait until the end of the article to see your clever solution that negated my angst. Well done, sir!
Years ago, i read an interview with an Ubuntu dev who said his job was to eliminate "papercuts". Papercuts aren't big bugs, but the little annoying things that add up and ruin your impression of a product. I am glad to see you're focusing on the little things that help make Drupal feel like a polished, papercut-less product.
Good job!
Thanks! Credit to frontend framework manager @nod_ for pushing me to find a solution to this, But yeah, I love the idea of eliminating "papercuts". Every little one matters!