Build · Craft
Performance and reduced motion

Performance and reduced motion

The blinking switchboard on the Next.js site glows. In Crafting the Next.js Website Rauno Freiberg explains that the glow is a CSS shadow, but that the only properties being animated are opacity and transform. Animating box-shadow would trigger a browser on every frame, which modern computers may absorb but older, less powerful devices like smartphones may not, dropping frames and making the motion look sluggish. So instead of animating the shadow, he toggles the opacity of pseudo elements that already carry one, with a data-state of off, medium, or high. The hover effect that spells NEXT is the same idea: an array of cherry-picked indices whose lights are replaced in sequence. Emil Kowalski states the general rule in Great Animations: “If our animations won’t run at 60 frames per second, everything else we’ve talked about becomes useless.” Animate with transform and opacity, which only trigger the composite step, rather than padding or margin, which trigger layout, paint, and composite; and when the main thread is busy, use hardware-accelerated CSS or the Web Animations API. His example is the Vercel dashboard, where an active-tab transition built with shared layout animations dropped frames while the browser loaded the next page and was fixed by moving it to CSS.

The Vercel hero in What will you ship? is performance decided by layering. It is stacked, in descending order, as heading, SVG triangle, CSS grid lines, SVG rays, CSS rainbow gradient, and a GLSL shader, and it does not need any client-side code to display its essence, which matters because something that central should paint fast. The shader is : it fades in gracefully after the page has loaded, its async nature lets it be code split, and light hardware detection skips it entirely on low-powered devices while the visual keeps its core. The pixel icons follow the same pattern. Having them as pixel data means they can be rendered as a canvas for a smaller DOM footprint and animation, with a base64 placeholder image as fallback costing about 1kb per icon, or as SVG when above the fold. Paco Coursey’s SVG Caching with <use> (2020) is a related economy: reuse already-rendered icon DOM through <use href="#id"> with a stable hashed id, which in his example cut the markup by about 40%, as long as the id is consistent between server and client to avoid a hydration mismatch.

Reduced motion on the Vercel page is handled with a distinction worth copying. Looping complementary animations, the floating cursors and blinking carets, are paused under prefers-reduced-motion. Interactions are not reduced, because they are explicit inputs and none of them produce extravagant, amplified movement. The looping ones are CSS keyframes, so rather than setting animation: none and interrupting them abruptly, the page uses animation-play-state: paused, which Rauno describes as pausing them gracefully. And one detail he says no one sane has any reason to notice: the caret’s opacity is forced to 1 inside the media query, so the blink cannot pause in the state where the caret is invisible. The whole rule is @media (prefers-reduced-motion: reduce) { .cursor, .caret { animation-play-state: paused; } .caret { opacity: 1 !important; } }. Emil’s reason for the media query in Great Animations is blunt: animations can make some people feel sick or get distracted, and his example component animates opacity only when the user prefers reduced motion.

Emil’s design engineering skill, published on GitHub as emil-design-eng and excerpted in Agents with Taste, adds two constraints for when visual craft and frame rate pull against each other. Blur can mask an imperfect transition (his Practical Tips table suggests it when something still feels off), but “Keep blur under 20px. Heavy blur is expensive, especially in Safari.” And to see what the eye only senses, temporarily increase durations to 2 to 5 times normal, step frame by frame in the Chrome DevTools Animations panel, and test on real devices; as he puts it in Good vs Great Animations, “playing the animation frame by frame, or in slow motion, helps you spot it.” Jim Nielsen slowed his ::view-transition-old() timing right down to see an outline bug earlier in this chapter, and Kathryn Gonzalez’s modal lesson, after testing mobile only in Chrome’s responsive mode, was to always test with real devices. Three people, same tool.

The essay that documents the Vercel site opens by naming the trade-off. The team’s north stars were performance, constraint in visual flair, and bridging pages aesthetically, and “if an animation or interaction didn’t perform well, felt pompous, or out of rhythm relative to the page, we didn’t build it.” Rauno is honest about where his attention wants to go: he would much rather spend time polishing an animation spring than tracking down what made the initial load slow. Then he asks the question this chapter has been circling: “is having a slow website with immaculate attention to visual craft desirable?” The elements almost everyone experiences (page speed, legible typography, information honesty, layout stability, focus states, auditory feedback, DOM order) are not glamorous to obsess over because they are invisible, and invisible work is the easiest to trade away when shipping quickly.

That is where the chapter lands. Jim Nielsen’s claim was that code is how you find a design, and this section has been a list of designs that could only be found in code: a glow that does not repaint, a hero that paints before its shader arrives, a caret that pauses visible. The craft that nobody sees is still craft, and it is the part that everybody feels. Animate opacity and transform. Layer so the essence paints first and the enhancement can be skipped. Pause loops instead of killing them, and leave explicit interactions alone. Slow things down to look, and look on a real device. When an animation costs the frame rate, do not build it; a website that feels fast to everyone is a better piece of visual craft than one that looks immaculate to the people with the newest hardware.