DEV Community

Cover image for Speed Needs Design, or: You can’t delight users you’ve annoyed
Taylor Hunt
Taylor Hunt

Posted on • Updated on

Speed Needs Design, or: You can’t delight users you’ve annoyed

To really sell streamed HTML’s impact, I should have shown the existing site’s design, but with streaming’s speed.

The demo’s design deviations

Unfortunately, it was impossible to have the existing design and the speed I needed. This post is about some of the differences.

If I were a good scientist, I’d have before/after traces for each deviation. Unfortunately, I do not — I was hastily testing and retesting on real hardware and a packet-throttled connection. If something felt slower, I tossed it.

If I’m wrong about something, I’ll happily accept corrections with a good source.

Design goals

Look, I’m jealous of native devs too. They get 60–120fps fullscreen animations with builtin platform support and minimum overhead.

But this is a website that sells food: I will relentlessly sacrifice delight for easier access. I can never truly catch up to native apps’ interaction richness, platform fidelity, and SFX flaunting. But I can beat native apps at the Web’s strength of getting things done, easily, with little fuss. I wasn’t just trying to outrace our existing site — I aimed at our native app too.1

I figured I could add flourishes and rise to meet interesting challenges after finishing the usable, accessible bones.

My amateur login page The professional login page
A basic login page, with a “Sign in to Kroger” heading, a username field, a password field, a “Forgot your password?” link, a “Stay signed in on this device” checkbox, and “Sign in”/“Create an account” buttons. A loading spinner in a sea of white.

Which design looks better, 10 seconds in?

That said, I’m no designer. A real designer could probably make these tradeoffs with better results.

Fonts’s Nunito .woff2 files are ≈14kB each, totaling 55.3kB.

If I found a smaller lookalike font, subset aggressively, and applied more arcane font optimizations, that could probably shrink to ~12kB total.2 But remember the 20kB budget? 60% of that for a decorative font is hard to justify.

System fonts also spend less time on the main thread: each webfont src causes a reflow (they can batch together, but don’t count on it). Usually the reflow isn’t awful, but you know who’s not too good for free performance? 👉 This dork! 👈

So, no webfonts.

But that was an easy decision: avoiding custom fonts is negative work. How about something harder?

Product carousels

Displaying products is ecommerce’s most important job. Which is why it was a problem that our scrolling product lists didn’t fit on the phone we sell:

With the Poblano screen dimensions, the product cards are too tall, and you can only see one at a time.

I know whitespace is an important part of design, but this is probably not what they meant.

That was the obvious reason to change, but there’s also subtler drawbacks:

Memory pressure and increased layout cost

Scroll panes use RAM and VRAM for hardware-accelerated scrolling — but we need those for fast <body> scrolling, too!

A long white bar of the total surface area of the vertically-scrolling page, with four other long bars poking out to the right for the horizontal scroll areas. One of them extends so far it’s not fully shown.

Current homepage in Chrome DevTools’ Layers view. It only rasterizes the non-blank parts, but still calculates layout for all areas.

Remember how iOS Safari needed opting-in for accelerated scrolling with -webkit-overflow-scrolling: touch? Now you know why.

Scroll trap on touchscreens

The lists take up so much vertical space that they become infuriating scroll traps on the demo phones, like mobile map embeds. (It doesn’t help that cheap touchscreens often get swipe direction confused at the beginning of a gesture.)

Reflow and pop-in

As new cards load on scroll, they all resize their height to match the tallest. Even when the shift is minimal, the necessary layout calculations still hiccuped on the demo phones.

Poor use of small viewport

The Poblano wasn’t the smallest screen I targeted. I wanted to preserve legibility down to the Apple Watch.

Instead, I displayed products like this:

The demo’s products displayed as a vertical list, in block-level flow as websites do by default.

No, it’s not very attractive. But it works a hell of a lot better.

You can probably spot several tricks to save space in the above screenshot: product sizes inline with the names, relying on text’s horizontal nature for more wrapping compactness, etc.

⌛ Other things I wanted to do, but ran out of time

I wanted to use shape-outside to flow text into the empty spaces of non-rectangular products, so I could make the image larger. (I had a burning desire to maximize how many onscreen pixels could be image pixels without sacrificing too much information density.) I might post how to do this, someday.

Later, the “stickiness” of scrolling lists was cited to me as an important quality. I wanted to recoup some of that by decorating the “See more” links at the end of each product list with thumbnails of what more was on that list, but ran out of time before the demo. C’est la vie.

box-shadow more like bourgeois-shadow

I know, I know. Shadows are fashionable. If used correctly they make interfaces more understandable. The interplay of light and shadow is the sum total of visual art itself.

But even from a strict design perspective, shadows aren’t all-upside:

  • The area needed to spread/blur shadows requires space around elements, which comes at a premium on the small screens I targeted.
  • Current popular use is subtle, which means they can never be the workhorse of a design. (Unless you make shadows the only design element, and that’s terrible.)
  • An affordance that doesn’t meet contrast requirements is questionably useful for the people who need it most. Or even most people: glaring sunlight, tired eyes, distracted attention, etc. need strong affordances, not minimal ones. (I’m not even sure how many people can notice trendy shadows, from a visual accessibility perspective.)

And from not a strict design perspective…

Historically, box-shadow is a career performance criminal. Probably not as much nowadays, unless some combination of inset, blur radius, border-radius, transparency, shadowed element size, screen resolution, browser, OS, or hardware falls off browsers’ fast paths.

A flame chart bar, with a legend reading: 341ms requestAnimationFrame callbacks; Type: Paint.

Modern advice mostly warns about transitioning or animating around shadows — especially since scrolling under them also counts, as Facebook learned.

(Yes, Google’s Material Design went all-in on blurring and animating lots of shadows. Have you seen how much effort they spend because of that?)

There are ways to work around shadows’ performance risks, which might even be worth the effort.

Coupons on, with shadows for each coupon and their primary action buttons.

But… man, these box-shadows just don’t seem that worth it. They’re subtle! Intentionally! If I have to plan how to protect users from them, there needs to be way more of a payoff. At least skeuomorphism was in-your-face.

If a design effect is only noticeable to young people with good vision, and it can unpredictably penalize people with cheap or overworked devices… then I don’t care how delightful it is. The risks conflicted with my goals and this was my stupid passion project, dammit. I didn’t trust them and I didn’t need them.

So, I dropped the shadows. …but not like that.

Homepage promotions

Carousel Tiles
An autorotating carousel with a pause button and 3 dots indicating how many slides it has. The first dot is darker to indicate the current slide. 3 tiles, showing the same amount of promotions as the carousel via the ancient and mystical art of “put them next to each other”.

Think about what code the carousel needs that tiles don’t:

  • Next/previous swiping vs. buttons, and switching between them
  • Position indicator
  • Pause/Play (required for accessibility)
  • (prefers-reduced-motion) check and mitigation
  • Accessibly hiding offscreen slides
  • Autoforwarding
  • Animation timing
  • Repositioning slides to the left when wrapping
  • Tab ↹ handling

No matter how efficiently those features are implemented, they’re still code users must download. (If users actually liked carousels that could be worth it, but, uh…)

But the demo didn’t use tiles either

Encoding each promotion as one big image has problems:

  • Scaled-down text becomes unreadable small viewports
  • Doesn’t work with High-Contrast Mode or other forced-colors
  • Takes too long to display anything on the target connection, which meant it was as good as not having it
  • Costs users more than I was comfortable with. Especially since I couldn’t aggressively compress the images without making their text look crusty.

So the demo went even further beyond, with an approach I’m calling “ribbons” just now. Here’s how those load over the target connection:

The text and colored backgrounds are visible instantly, even though the images take a few seconds. (A media query rejiggers them into tiles on larger viewports.)

No modals, tooltips, toasts, etc.

Unlike the parts of this post where it felt like I was compromising, this was a user experience improvement. Do you like dealing with modals and pop-up banners and all their annoying friends? Me neither.

That’s an opinion, though. Some more objective reasons I eschewed widgets for boring alternatives:

  • Their JavaScript and CSS eat away at the performance budget
  • They make less sense on small/touch screens — in particular, modals take up nearly the entire page anyway
  • They’re hard to make accessible (modals in particular have been called “the final boss of web accessibility”)

More on that last one… Even if I did succeed in making widgets accessible, the JavaScript required to do so really adds up. Check out the sizes of some popular modules, each a reasonable choice for tooltips, modals, and toasts respectively:

JS cost according to Bundlephobia
Module Minified .min.gz Slow 3G download
@popperjs/core 2.11.4 20.5 kB 7.2 kB 144 ms
@reach/dialog 0.16.2 27.3 kB 9.4 kB 188 ms
notistack 2.0.3 18.8 kB 6.4 kB 128 ms
Total 66.7 kB 23 kB ~½ second!

✏️ Note: this table doesn’t include parse/execute time, which scales with minified kB.

(Yes, there are more efficient ways to script these widgets. I just grabbed whatever looked well-maintained and popular, like the vast majority of devs.)

Finally, implementing complex interactivity would have taken a lot out of me. Check out what it took for Pedro Duarte to make an accessible dropdown:

Dropdown Menu: 2,000+ hours, 6 months, 50 reviews, and 1,000s of commits.

Source: Headless components in React and why I stopped using a UI library for our design system

But if these UI patterns are annoying, not very accessible, and costly, why do so many sites use them?

  • I think they’re easier to design, since they don’t have to care about the underlying page.
  • I suspect analytics falsely report increased “engagement” because they need more interaction than less intrusive designs. Even if said interaction is, say, users trying to get the damn carousel to hold still.
  • Sometimes, they can be the best solution for a problem — but as soon as you have that component, it’s tempting to reuse it for other, less-suited problems.

More on alternatives to these known user-aggravators:

Better design via speed

Unlike the other sections where design was in service to speed, this section is about how fast page loads can enable better design!

Our existing checkout flow had expanding/contracting accordions, intertwingled error constraints, and tricky .focus() management because of the first two. I wanted to avoid all that, so I broke up checkout into a series of small, quick pages:

The pages of my checkout flow: 1. My shopping cart, 2. Order summary, 3. Contact information, 4. Thank you for choosing Pickup!

If you’ve heard of One Thing Per Page, that’s what I did. It didn’t take long to code, and was surprisingly easy.

But wait! There’s more!

  • low-confidence users find them easier to use
  • they work well on mobile devices
  • they’re better at handling things like errors, branches, loops and saving progress

— One thing per page · Design in government

I didn’t implement them for the demo, but functionality like user’s Account info and settings would also be well-served by this pattern.

[W]hen we started user research with the general public, we saw a very positive response to the simple step by step approach, even on large screens. Though it added more clicks, people said it made the process feel simple and easy - there wasn’t too much to take in and process at any one time. So we stuck with the simpler screens for everyone.

— GOV.UK: Things we learnt designing ‘Register to vote’

And thanks to Paint Holding these page sequences looked and felt as seamless as SPA navigation. (Lots more on that in the next post.)

So what?

Like how woodworking involves sculpting along the grain, web design needs an understanding of what’s easy and natural, to save effort for things that really need it.

Our current practices of pushing designers away from HTML & CSS thwarts an intuitive understanding of what’s easy vs. what’s hard on the Web. More cooperation and faster feedback loops could help — because I was designing and developing myself, the loop was as tight as possible as I switched hats on the fly.

But, uh, it’s also true my designs won’t win any beauty contests. So don’t trust me, listen to some actual designers:

In the next post, I reveal that while I may be a twentysomething paid to work in React, I’m secretly a filthy progressive enhancement liker.

  1. Native has much improved since Gruber wrote that post. It’s time web devs also stepped up our game, and not by playing catch-up: “What I missed when I dismissed them a decade ago is that web apps don’t need to beat desktop apps on the same terms.” 

  2. Based on results with Poppins from an earlier design. 

Top comments (7)

peerreynders profile image
peerreynders • Edited

Thanks for another great article and including a plethora of useful links. I also like the ongoing cost-benefit analysis. It's a refreshing change from the usual "there's a dependency for that" style of problem solving which doesn't look at the downstream costs that can be incurred.

I can never truly catch up to native apps’ interaction richness, platform fidelity, and SFX flaunting.

This also reminded me of Web vs. native: let’s concede defeat (which also happens to reference Mobile Apps Must Die).

"I feel we’ve gone too far in emulating native apps. Conceding defeat will force us to rethink the web’s purpose and unique strengths — and that’s long overdue."

SPAs were seen as a disrupting force on the web, at a time were disruption was deemed necessary to avoid stagnation. I think this was just about the time Gen 0 was coming to grips with Progressive Enhancement.

Gen 3 looks like it could be a course correction, countering that disruption but also learning from Gen 1 and Gen 2.

paid to work in React, I’m secretly a filthy progressive enhancement liker.

Somehow I suspect you didn't start with React because it doesn't guide you to this kind of knowledge.

tigt profile image
Taylor Hunt

You’re absolutely right. I started with SVG to make webcomics, which I understand is extraordinarily atypical.

philw_ profile image
Phil Wolstenholme

I wanted to use shape-outside to flow text into the empty spaces of non-rectangular products, so I could make the image larger. (I had a burning desire to maximize how many onscreen pixels could be image pixels without sacrificing too much information density.) I might post how to do this, someday.

This sounds intriguing! Thank you for another very interesting (and fun to read!) post

tigt profile image
Taylor Hunt • Edited

It’s like using shape-outside: url(…), but without needing the image to load — the idea was to inline style="shape-outside:polygon(…)" along the width and height attributes to avoid layout shift.

The steps without involving code are:

  1. Resize image to 100×100 pixels
  2. Run a concave-hull algorithm on the image to get the points of the polygon
  3. Convert to CSS polygon(x1% y1%, … x𝑁% y𝑁%)
  4. Stash that wherever you store the image’s URL so you can load them all at the same time
svgatorapp profile image

"In the next post, I reveal that while I may be a twentysomething paid to work in React, I’m secretly a filthy progressive enhancement liker." Looking forward to reading that one as well :)

chriskt profile image

Super well-written article and a nice guy, to boot! 👏

ktsangop profile image

Wow. Just impressive.
Struggling with web app performance at the moment, and got some really useful tips here.
Thank you so much :)