Tag: Users

Help Users Accomplish What They Came For

From my perspective, the question of what one thing we can do to make a website better is not a technical one. The more I browse the internet, the more I realize that the biggest issue with a lot of websites is the fact that they don’t let me accomplish the task I am looking to get done. Whether it is the usability or the information architecture or the performance it doesn’t really matter. Over the years, new browser capabilities and the tech stack have made it possible to add more and more complexity to an average website. We see it everywhere: in pages presenting the product, in booking services, in portfolios, and in online shops. We try to delight the user instead of focusing on one simple task: helping the user accomplish what they came to do.

If I were to point out one thing that people can do to make their website better, it is to take a moment to think about the most crucial actions that we want our users to be able to do on a page and make them as easy and accessible as possible.

All visual effects, fancy graphics, beautiful interactions, and tracking scripts should come second.

I can give you an example from my own experience. A few years ago, I went on holiday in a remote area with very limited internet access. My luggage was lost and there were not many places where I could buy extra clothing or cosmetics. I could not find where my luggage was or when it would be delivered because the airline website did not load on my limited data—it would not even show a phone number I could call, and the email address I found someplace else turned out to be outdated. The website did not follow the rules of progressive enhancement and graceful degradation; it only allowed privileged users with a good enough internet connection to download a huge amount of JavaScript that’s responsible for building the whole experience. In their case, a simple form with two text inputs and basic text information as a fallback would have easily solved my issue. I can bet that the developers spent countless hours making the experience delightful, yet I was unable to even see it.

It is easy to get caught up in the moment and follow the milestones for the project as they are described through tickets in Jira or some other project management software. It is easy to reuse the solutions we are used to. and we can easily copy/paste from previous projects or Stack Overflow. It’s also easy to assume that if something “works on my machine” it will also work for everyone else.

What’s difficult is taking a moment to look past the features that add new value to the project and to focus on parts of the app that may have been overlooked in the process. It is hard to stay on top of things like new features and browser APIs are being released. It is hard to think that someone might not have the same privilege as we do.

Take a moment to rethink what is its true value to the user visiting and to try to look at the page with a fresh eye.

It can be challenging as we get used to the solutions we built. It is hard for us to imagine how people can fail to follow the instructions or clues we left for them on the screen, or imagine how the page might feel to an unsighted user or someone who can only navigate using the keyboard. We forget to test edge cases and anything that goes beyond the “happy path” of the user and instead tend to overlook the fact that we are using a powerful MacBook with a sharp display and internet flowing at a steady pace. We forget that some people are not native English speakers and consider that a word that is self-explicable to us can mean nothing to a user who does not use it in everyday conversation.

I challenge you to take time to look at your website as if it was your first time around.

Use it in the production environment with the stream of third-party resources that might not be there when you are in the development mode. Use it with a very poor internet connection and measure how long it takes to accomplish a simple task like filling a form. Try using it with a different device you might have not used before.




I challenge you to find a real user of your website and take a moment to watch how they use what you built during a user testing session.

You probably have some assumptions about what causes headaches for your users and what doesn’t. I could bet that some of these assumptions will be challenged and you wind up creating a whole list of things to fix that you wouldn’t otherwise consider.

I hope that progressive enhancement doesn’t become yet another buzzword and that you really take a moment to help the user accomplish what they came for. If you are interested in learning more on this topic I can recommend getting familiar with one of Jeremy Keith’s presentations on that topic or the article by Aaron Gustafson that popularized the idea.


, , , ,

Test Your Site With Real Users

A few years ago, there was this French book publisher. They specialize in technical books and published an author who wrote a book about CSS3, HTML5 and jQuery. The final version, however, a glaring typo on the cover where “HTML5” was displayed as “HTLM5.” Read that twice. Yes. “HTLM5.” (Note that it was also missing the capitalized “Q” in jQuery in one version.)

Image of the book containing the typo. It has three cartoonish figures on it dressed as superheroes, then a product description of the book to the right of the cover.

I don’t know how many people are involved in publishing and printing a book. I bet quite a few. Yet, it looked like none of the people involved saw the typo. It made it to the printer, after all.

And this kind of thing happens all the time on projects. One of my favorite French expressions is avoir la tête dans le guidon. A literal translation is “having your head in the handlebar.” (The English official version is having your nose in the grindstone.) It comes from cycling. When cyclists are trying to win a race, at some point, they end up with their nose so close to the handlebar that nothing else around them matters. They are hyper focused on the road ahead. They can’t see anything else around anymore.

Photo of a cyclist in a black helmet and red jacket on a black and blue racing bike riding through a busy intersection with a blurry backdrop indicating a fast speed.
Credit: Max Bender via Unsplash

And this is exactly what happens to us quite often on projects. We and our teams are so focused at some point on shipping the site (or printing the book) that we get blindfolded and fail to see little (or big) details anymore. This is how you ship a book about “HTLM5” and a website with navigation issues and dead ends in user flows, or features no one needs.

Gaining an external view with user testing

If you want to avoid these sorts of things, you need an external view of your site, product or service. And the best way to gain that view is to test it with people who are not on the team. We call this usability or user testing. I have to confess that I’m biased here since part of my job is to perform user testing on websites. So, I have to say that, ideally, you want to test with your target audience — the people who actually use your website, product, or service. But, if (and this is a big if) you can’t find any users, at least have a first round of tests with people who did not work directly on the project.

You also want to test with people with different impairments to make sure the end result is as accessible as possible.

When should I start testing my project?

In a perfect world, you test as soon and as often as possible. Testing prototypes built in design tools before starting development is cheaper. If the concept doesn’t work, at least you did not invest three months of development into an ineffective feature.

You can also test HTML/CSS/JavaScript prototypes with fake data built for the tests — or test once the feature or website is developed. This does mean, though, that any changes are more complex and expensive.

Define what you want to test

The first step is to define what specific tasks or activities you want to test. Usually, you want a set of different actions with a user goal at the end. For example:

  • an account creation process
  • a whole checkout process
  • a search process from the homepage to the final blog post, etc.

List the tasks and activities the user needs to accomplish in the form of questions. We call this a creating a test script. You can find an example here from 18F.

Be careful not to bias users. This is the tricky part. For example, if you want to test an account creation flow and the button says “Sign up,” then avoid asking your test users to “sign up” because the first thing they will do is search for a button with the same verb on the screen. You could ask them to “create an account” instead and gain better insights.

Screenshots of Axure and Word side by side.
Example of a protype build in Axure and a test script

Then prepare the prototype you want to test. As mentioned before, it can range from mockups with a click-through prototype to a fully-developed prototype with real data. It’s totally up to you and how far you are in the project. Just make sure it works (technically, I mean).

Recruit participants

You know who your users are on most of your projects. The question is: how can you reach out to them? There’s plenty of ways. You might go through support or salespeople with lists of possible participants. If it’s a broad target audience, you could recruit testers right where they are. Working on an e-commerce website that sells plants? Why not try visiting actual physical shops, online communities for gardeners, community gardens, Facebook groups, etc.

You can use social media to recruit participants as long as you recruit the right people who are prospective users of the site. This is why UX professionals use screeners. A screener is a set of questions you while recruiting (and when starting the test), to make sure you are working with someone who is in the target audience.

Note that participants are usually compensated for their time. It can be gift cards, maybe getting of your product, some really nice chocolate — something that encourages people to spend time with you in a way that thanks them.

If you struggle recruiting and have a budget, you can use professional user research recruitment websites like userinterviews.com or testingtime.com.

Schedule, set up, prepare

Once you successfully recruit participants for testing, schedule a meeting with them, including the testing date, time, and place. The test can be remote or face to face. I won’t detail the logistics here, but at some point, you will need help to set up an actual room or a virtual space for the testing. If it’s a physical room, make sure it’s calm and accessible for your users. If it’s remote, make sure the tools are accessible and people can install them if needed on their computers.

Schedule some emails in advance to remind participants the day before the test, just in case.

Last but not least: do a dry run of your test using people from your team. This helps avoid typos in the scripts and prototypes. You want to make sure the prototype works properly, that there are no technical issues, etc. You want to avoid anything that could bias the test.

Facilitate the test

You need two testers to conduct a usability test. One person facilitates. The other takes care of the logistics and notes.

Welcome the participant. You can find a lot of templates for usability testing over at usability.gov, including consent forms, email template examples, and much more.

Start the recording, but only if they give you permission to do so, of course. Explain that you are testing the site, not them, and that there are no right or wrong answers. Encourage them to think out loud, and to tell you exactly what they do, see, and think.

Put them at ease by starting with a few soft questions to get them to talk. Then follow your script.

The most important thing: don’t help users accomplish the tasks. I know, this is hard. We don’t like to see people struggle. But if you help them, you will bias the results. Of course, if they struggle for five minutes and you need them to accomplish the task to go to the next one, you can unlock them. Mark that particular task as “failed.”

Once testing is finished, thank the test user for their time and offer them the compensation (or tell them how to get compensated if it was a remote test).

Get the recording, upload it somewhere in the cloud so there is a backup. Same for your notes. Trust me on that, there’s nothing worse than losing some data because the computer crashed.

Analyze and document the results

After the test, I usually like to put together a quick “first draft” of the analysis for a given participant because the testing is still fresh in my mind.

Some people do this in shared documents or Excel sheets. My favorite method is using the actual screens that were used for testing in a Miro board. And I put digital sticky notes on them with the test’s main findings. I use different colors for different types of feedback, like a user comment, feature request, usability issue, etc.

When multiple users give the same feedback or experience the same issue, I add a small dot on the note. This way, I have a visual summary of everything that happened during all the tests.

Screenshot of mockup screens in Miro with notes attached to various areas of the screens. There are 13 total screens, each with different layouts and content.

And then? Learn, iterate, improve.

We don’t test for the fun of testing. We test to improve things. So, the next step is to learn from those tests. What worked well? What can be improved? How might we improve? Of course, you might not have the time and budget to improve everything at once. My advice is to prioritize and iterate. Fix the biggest issues first. “Big” is a relative term, of course, and that depends on your project or KPIs. It could mean “most users have this issue.” Or it could mean, “if this doesn’t get fixed, we will lose users and revenue.” This is when it becomes again, a team sport.

In conclusion

I hope I’ve convinced you to test your site soon and often. This was just a small introduction to the world of testing with real users. I simplified a lot in here to give you an idea of what goes into user testing. Proper professional usability testing is more complex, especially on large projects. While I always favor hiring someone dedicated to user research and testing, I also understand that it might be complicated for smaller projects.

If you want to go further, I recommend checking out the following resources:


, , ,

Stealing Game Animation Techniques to Engage Users

Today’s websites are overflowing with animations—often too many. They get in the way of the content and slow down our busy users. But at the same time: they’re wonderful. They bring websites to life, are fun to implement and can be incredibly impressive to show off. I think they’re great. Sorry impatient users.

The way I see it, the problem isn’t necessarily that websites have too many animations, but that the animations don’t vibe with the content they’re promoting. They’re out of place with their subject matter. They feel contrived and provide no additional value.

This is an article for web developers who want to get fancy-shmancy with the finest animations around, but don’t want to do it at the cost of annoying users. I’ll show you some of the ways I’ve personally used website animations while trying to annoy very few users. You might be thinking that “not annoying users” is a very low bar that I’ve set and well, uh.. hmm.. yes. Good point.

This is a topic I’ve somewhat stumbled my way into. I work as a web developer for an indie video game publisher called Devolver Digital. I, along with Vieko, make websites for these video games. While I’m primarily a dev, a good chunk of my job is to conceptualize and create designs. I once thought that design didn’t seem that hard, the truth is, as most of you probably know, it’s not that straightforward. It’s really, really hard.

My first design for the Ape Out website 😬

When I first started making video game websites, I would sketch it out in a notebook. This would be fine, but then when I tried to implement the design in Photoshop, it just wouldn’t come together. I would restart from scratch and try again. It still wouldn’t work. Then I would try to skip the whole design step and jump into the code, hoping that the animations would bring it all together. The animations were cool, but it still wasn’t good enough. My design skills were lacking.

But then I figured out a way to fake it.

Now, when I show website concepts to my colleagues and clients for feedback, they tell me that I’m the best designer in the whole world and they send me cookies. They don’t know that I’m hiding a terrible secret.

I don’t know if I should reveal this secret. Web designers around the world are going to hate me. My colleagues will ask for their cookies back. OK—I ate the cookies. I’ll let you in on the secret. The secret is that…

I copy everything from video games.

I copy the colors, the buttons, the modal boxes, and even the core concepts and gameplay mechanics. I play the games (they’re fun), take screenshots, capture footage, and then I steal it all. Most important of all, I steal the animations.

While video games make for a perfect medium from which to burgle, I believe that people like you can also steal from your corresponding industry. While stealing from games helped me cheat as a designer, it was stealing the animations that really unlocked the full potential of each website.

It’s free real estate

We, at Devolver Digital, have a massive variety of games. Some are calming, narrative experiences, while others have very intriguing gameplay elements. Some have very simple visuals that hide deep secrets, and others are Shadow Warrior 3.

Each one of these games has a unique way of wiggling themselves into a player’s brain and setting up a cozy little living space. With each website, we attempt to replicate this brain connection and harness it. Modern web tech is at a place now where we can make websites that effectively engage our users into making this brain connection before they’ve even grabbed a gamepad.

So basically, we heist assets/animations/mechanics/everything from the game to give the user a glimpse at what it would be like to play it.

For example

I want to start with one of my favorite projects. It’s the website for a game called Olija. It’s about a harpoon-wielding hero exploring hostile lands to find his way home. The game has two core aspects. The first is its visual fluidity. The pixel size in its pixel art is fairly large, so it can be difficult to convey this game’s beauty without seeing it in motion. The second core aspect is its story. It feels like the game is based on a pre-existing book or TV series.

The goal for the website was to take these two core aspects, and entice the user to engage with them; to make them feel like a part of the story and to make them feel in control of the action.

I’m going to jump away from Olija for a second here. I should have mentioned this earlier. Websites have an aspect that gives them an advantage over most other media: they’re interactive (obviously). This is great because we can use this interactivity to get the user to engage with the site at a deeper level. This is key. We have a few tools we can use to make their experience more vibrant and memorable. These tools are things like mouse movement, mouse hovers, pointer down/up states, the scrollbar, and the keyboard. My favorite is the scrollbar. You’ll see it used often in my projects. I feel it’s the most intuitive to use and I like that it requires little thought or active energy from the user.

Back to Olija. One of the first things the user sees upon scrolling down is a movie-credits-like fade animation. It’s slow. It takes three viewport heights (300vh) to scroll through it all but it’s a critical part. It sets up the pacing and immediately shows the tone of the game. It’s a very simple animation but it was vital. The rest of the page felt much more natural once this section was extended and slowed down.

Olija credits sequence

After the story sections, the user gets to dynamic screenshot-like sections. There’s one section in particular in which the hero runs across a lively forest backdrop. Showing a lighter moment like this one reinforces the epic-ness of the story and shows off the game’s unique style.

One of the trickier parts for Olija, is that pixel-art is based on sprite sheets. We can’t just animate an element’s transform properties to move it around the screen. We have to also animate its current position in the sprite sheet. Here’s a Pen that shows how we did it.

We took a similar approach with Ape Out. The game feels like an action movie. As the game’s title accurately describes, the player is an ape trying to escape from various scenarios. The idea for the accompanying website was to show a scene that had already been played out, letting the user imagine all the action that leads up to the final epic moment. Again, they control the pace of the camera that explores this scene with the scroll bar.

Ape Out Camera Pan

At first glance it might look like a 3D WebGL canvas, but it’s actually a lot of divs placed with a 3D transform where the corresponding perspective origin is updated based on the scroll position. Not sure if it’s any better or easier than using ThreeJS but it’s.. uhh.. doable. Here’s a barebones version of it in CodePen.

Enter the Gungeon in the Living Room (Illustration by Björn Feldmann)

As a last example for story-driven experiences, I want to mention the Enter the Gungeon website. It’s a mix of the Olija and Ape Out ideas, but instead of trying to capture the game’s narrative, it attempts to celebrate the time players have spent with it. The goal was to slow down and let the user reminisce, then use this nostalgia to promote the Exit the Gungeon and House of the Gundead games.

Unique selling point

Sometimes games have a unique hook or gameplay element and the website is the perfect place to showcase it. A great example is Loop Hero. It’s a very simple game on the surface. The hero automatically travels along a looping path and fights the monsters they encounter.

The player controls the hero’s equipment and whether they abandon the path to return to the village. This made infinite looping a clear concept for the website. When the user reaches the bottom of the page they are seamlessly transported back to the top and the page resets. The scene in the viewport is exactly the same so the user is none-the-wiser.

Loop Hero’s looping website

There are a few other concepts on this site that are directly taken from the game. Once the user scrolls to the battle trigger area, the scrollbar is disabled (sorry, I know it’s bad) and they are forced to watch the battle play out. The hero and monsters each have health points, attack speed, and attack power values with random variance. This means that the battle outcomes are not hard-coded. Each time the user loops through the page, the background map is updated with new tiles to show how the game progresses and evolves.

One of my favorite features of the Loop Hero website is the fade effect between the top section and the looped road section below it. The game developer at Four Quarters sent me the shader code (written with help from @kartonnnyi) and the Perlin Noise image used for this effect. I can’t say I fully understand how it all works, but I was able to put it together with gl-react.

People with a deep understanding of how to write shaders have my deepest respect. I don’t think I’ll ever fully be able to wrap my head around them. The stuff at Shadertoy blows my mind.

Boomerang X and Shadow Warrior are similar websites that use gameplay footage as a direct way to show off the game. They both engage the user with elements connected with the background video. The Shadow Warrior website content shakes when there’s an impact in the video and blood slowly gathers on the menu/logos. The Boomerang X logo zips back and forth along with the game footage’s boomerang. The logo is further connected with the user by having it react to the user’s mouse position.

Alright, now let me tell you about Devolver Tumble Time. I think most web developers, upon being given the opportunity to make a website for this game, would feel an urge to replicate the tumbler mechanic. From first-hand experience, I can say that it’s not as simple as it looks. I knew that it was critical to get the tumbler to run as fluidly as possible, otherwise it would reflect poorly on the game and turn people away.

I started out with MatterJS. It seemed like the obvious choice. The tumbler is two-dimensional, so pick the most popular 2D engine right? Well, it turns out the tumbler isn’t 2D and I don’t think centrifugal force is a thing in MatterJS. I, like another soul before me, attempted to hack it in. I tried to implement their attempt unsuccessfully.


To get the tumbler to work, I needed two directions for the gravity. The items should fall down the y-axis, but they also need to be able to “stick” to a spinning disc through gravity/friction on the z-axis. I spent a weekend refactoring the tumbler code in ThreeJS and achieved the desired fluidity.

Down into the abyss

I want to end this article with a somewhat deep-dive into the Phantom Abyss website. It encompasses a lot of the techniques used in recent projects and it only weighs 4MB.

The website might seem fairly barebones, but the longer the user looks at the page, the more there is to see. The shifting blocks and fading phantoms should be obvious, and then the user might see that the waterfalls are animated, along with some dust particles, the torches, the woman’s hair and whip, and then lastly the phantom’s eyes above the logo, a sparkle on some of the relics, and the fires and birds in the far background.

Phantom Abyss Animations (Illustration by Dan Mumford)

Now, I’m definitely not expecting many users to notice all these minor details, but they bring depth to the page. They engage the user into subconsciously understanding that the game holds secrets and depth.

This is the first project on which I used SVGator, a really neat tool for animating SVGs. I used this for the torches, the woman’s hair/whip, and the phantom’s blinking eyes.

The torches were not easy. My first attempt looked like creepy wild tentacles reaching out to grab whatever they could find. My second attempt was better but not quite there. My third attempt still wasn’t good enough, but adding blur and some sparks brought it to the point where it is today. If you look very closely the torches still look a bit like tentacles. In my next project I saw that the key-art featured a lot of tentacles. I knew what I had to do.

Start inside

These are just a few of the examples of animation tricks we’ve used on websites. Devolver Digital has a never-ending catalog of games and from that site you can check out other websites we’ve made. Ragnorium, Heave Ho, and Gato Roboto are some of our sites that stole a lot from their games. Also, full credit to Vieko as he was the original game animation burglar with his sites for Minit and Sludge Life.

Our sites are mostly all hosted on Vercel ❤ with NextJS 👌. We also rely heavily on Framer Motion 🤩 and often react-three-fiber 🎆. There are dozens of alternative tools that are also great but we love these ones and they’ve made our lives much easier.

Today, we developers have access to an immense set of tools and techniques with new ones being announced all the time. It’s very easy to get caught up in what other talented devs are doing and we then try to replicate their effects. This isn’t necessarily a bad approach, but it can lead to being closed-off and repetitious. This causes us to then create animations that users have already seen dozens of times.

Instead of looking at other devs and their work for inspirational animations and transitions, we should look at the content and subject matter we’re showcasing. Invent completely new animations. We should take a step back and really contemplate how we can engage the users to connect with our websites and deepen their experiences.

The post Stealing Game Animation Techniques to Engage Users appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.


, , , , ,

Give Users Control: The Media Session API

Here’s a scenario. You start a banging Kendrick Lamar track in one of your many open browser tabs. You’re loving it, but someone walks into your space and you need to pause it. Which tab is it? Browsers try to help with that a little bit. You can probably mute the entire system audio. But wouldn’t it be nice to actually have control over the audio playback without necessarily needing to find your way back to that tab?

The Media Session API makes this possible. It gives media playback access to the user outside of the browser tab where it is playing. If implemented, it will be available in various places on the device, including:

  • the notifications area on many mobile devices,
  • on other wearables, and
  • the media hub area of many desktop devices.

In addition, the Media Session API allows us to control media playback with media keys and voice assistants like Siri, Google Assistant, Bixby, or Alexa.

The Media Session API

The Media Session API mainly consists of the two following interfaces:

  • MediaMetadata
  • MediaSession

The MediaMetadata interface is what provides data about the playing media. It is responsible for letting us know the media’s title, album, artwork and artist (which is Kendrick Lamar in this example). The MediaSession interface is what is responsible for the media playback functionality.

Before we take a deep dive into the topic, we would have to take note of feature detection. It is good practice to check if a browser supports a feature before implementing it. To check if a browser supports the Media Session API, we would have to include the following in our JavaScript file:

if ('mediaSession' in navigator) {   // Our media session api that lets us seek to the beginning of Kendrick Lamar's "Alright" }

The MediaMetadata interface

The constructor, MediaMetadata.MediaMetadata() creates a new MediaMetadata object. After creating it, we can add the following properties:

  • MediaMetadata.title sets or gets the title of the media playing.
  • MediaMetadata.artist sets or gets the name of the artist or group of the media playing.
  • MediaMetadata.album sets or gets the name of the album containing the media playing.
  • MediaMetadata.artwork sets or gets the array of images related with the media playing.

The value of the artwork property of the MediaMetadata object is an array of MediaImage objects. A MediaImage object contains details describing an image associated with the media. The objects have the three following properties:

  • src: the URL of the image
  • sizes: indicates the size of the image so one image does not have to be scaled
  • type: the MIME type of the image

Let’s create a MediaMetadata object for Kendrick Lamar’s “Alright” off his To Pimp a Butterfly album.

if ('mediaSession' in navigator) {   navigator.mediaSession.metadata = new MediaMetadata({     title: 'Alright',     artist: 'Kendrick Lamar',     album: 'To Pimp A Butterfly',     artwork: [       { src: 'https://mytechnicalarticle/kendrick-lamar/to-pimp-a-butterfly/alright/96x96', sizes: '96x96', type: 'image/png' },       { src: 'https://mytechnicalarticle/kendrick-lamar/to-pimp-a-butterfly/alright/128x128', sizes: '128x128', type: 'image/png' },       // More sizes, like 192x192, 256x256, 384x384, and 512x512     ]   }); }

The MediaSession interface

As stated earlier, this is what lets the user control the playback of the media. We can perform the following actions on the playing media through this interface:

  • play: play the media
  • pause: pause the media
  • previoustrack: switch to the previous track
  • nexttrack: switch to the next track
  • seekbackward: seek backward from the current position, by a few seconds
  • seekforward: seek forward from the current position, by a few seconds
  • seekto: seek to a specified time from the current position
  • stop: stop media playback
  • skipad: skip past the advertisement playing, if any

The MediaSessionAction enumerated type makes these actions available as string types. To support any of these actions, we have to use the MediaSession’s setActionHandler() method to define a handler for that action. The method takes the action, and a callback that is called when the user invokes the action. Let us take a not-too-deep dive to understand it better.

To set handlers for the play and pause actions, we include the following in our JavaScript file:

let alright = new HTMLAudioElement();  if ('mediaSession' in navigator) {   navigator.mediaSession.setActionHandler('play', () => {     alright.play();   });   navigator.mediaSession.setActionHandler('pause', () => {     alright.pause();   }); }

Here we set the track to play when the user plays it and pause when the user pauses it through the media interface.

For the previoustrack and nexttrack actions, we include the following:

let u = new HTMLAudioElement(); let forSaleInterlude = new HTMLAudioElement();  if ('mediaSession' in navigator) {   navigator.mediaSession.setActionHandler('previoustrack', () => {     u.play();   });   navigator.mediaSession.setActionHandler('nexttrack', () => {     forSaleInterlude.play();   }); }

This might not completely be self-explanatory if you are not much of a Kendrick Lamar fan but hopefully, you get the gist. When the user wants to play the previous track, we set the previous track to play. When it is the next track, it is the next track.

To implement the seekbackward and seekforward actions, we include the following:

if ('mediaSession' in navigator) {   navigator.mediaSession.setActionHandler('seekbackward', (details) => {     alright.currentTime = alright.currentTime - (details.seekOffset || 10);   });   navigator.mediaSession.setActionHandler('seekforward', (details) => {     alright.currentTime = alright.currentTime + (details.seekOffset || 10);   }); }

Given that I don’t consider any of this self-explanatory, I would like to give a concise explanation about the seekbackward and seekforward actions. The handlers for both actions, seekbackward and seekforward, are fired, as their names imply, when the user wants to seek backward or forward by a few number of seconds. The MediaSessionActionDetails dictionary provides us the “few number of seconds” in a property, seekOffset. However, the seekOffset property is not always present because not all user agents act the same way. When it is not present, we should set the track to seek backward or forward by a “few number of seconds” that makes sense to us. Hence, we use 10 seconds because it is quite a few. In a nutshell, we set the track to seek by seekOffset seconds if it is provided. If it is not provided, we seek by 10 seconds.

To add the seekto functionality to our Media Session API, we include the following snippet:

if ('mediaSession' in navigator) {   navigator.mediaSession.setActionHandler('seekto', (details) => {     if (details.fastSeek && 'fastSeek' in alright) {       alright.fastSeek(details.seekTime);       return;     }     alright.currentTime = details.seekTime;   }); }

Here, the MediaSessionActionDetails dictionary provides the fastSeek and seekTime properties. fastSeek is basically seek performed rapidly (like fast-forwarding or rewinding) while seekTime is the time the track should seek to. While fastSeek is an optional property, the MediaSessionActionDetails dictionary always provides the seekTime property for the seekto action handler. So fundamentally, we set the track to fastSeek to the seekTime when the property is available and the user fast seeks, while we just set it to the seekTime when the user just seeks to a specified time.

Although I wouldn’t know why one would want to stop a Kendrick song, it won’t hurt to describe the stop action handler of the MediaSession interface:

if ('mediaSession' in navigator) {   navigator.mediaSession.setActionHandler('stop', () => {     alright.pause();     alright.currentTime = 0;   }); } 

The user invokes the skipad (as in, “skip ad” rather than “ski pad”) action handler when an advertisement is playing and they want to skip it so they can continue listening to Kendrick Lamar’s “Alright track. If I’m being honest, the complete details of the skipad action handler is out of the scope of my “Media Session API” understanding. Hence, you should probably look that up on your own after reading this article, if you actually want to implement it.

Wrapping up

We should take note of something. Whenever the user plays the track, seeks, or changes the playback rate, we are supposed to update the position state on the interface provided by the Media Session API. What we use to implement this is the setPositionState() method of the mediaSession object, as in the following:

if ('mediaSession' in navigator) {   navigator.mediaSession.setPositionState({     duration: alright.duration,     playbackRate: alright.playbackRate,     position: alright.currentTime   }); }

In addition, I would like to remind you that not all browsers of the users would support all the actions. Therefore, it is recommended to set the action handlers in a try...catch block, as in the following:

const actionsAndHandlers = [   ['play', () => { /*...*/ }],   ['pause', () => { /*...*/ }],   ['previoustrack', () => { /*...*/ }],   ['nexttrack', () => { /*...*/ }],   ['seekbackward', (details) => { /*...*/ }],   ['seekforward', (details) => { /*...*/ }],   ['seekto', (details) => { /*...*/ }],   ['stop', () => { /*...*/ }] ]   for (const [action, handler] of actionsAndHandlers) {   try {     navigator.mediaSession.setActionHandler(action, handler);   } catch (error) {     console.log(`The media session action, $ {action}, is not supported`);   } }

Putting everything we have done, we would have the following:

let alright = new HTMLAudioElement(); let u = new HTMLAudioElement(); let forSaleInterlude = new HTMLAudioElement();  const updatePositionState = () => {   navigator.mediaSession.setPositionState({     duration: alright.duration,     playbackRate: alright.playbackRate,     position: alright.currentTime   }); }   const actionsAndHandlers = [   ['play', () => {     alright.play();     updatePositionState();   }],   ['pause', () => { alright.pause(); }],   ['previoustrack', () => { u.play(); }],   ['nexttrack', () => { forSaleInterlude.play(); }],   ['seekbackward', (details) => {     alright.currentTime = alright.currentTime - (details.seekOffset || 10);     updatePositionState();   }],   ['seekforward', (details) => {     alright.currentTime = alright.currentTime + (details.seekOffset || 10);     updatePositionState();   }],   ['seekto', (details) => {     if (details.fastSeek && 'fastSeek' in alright) {       alright.fastSeek(details.seekTime);       updatePositionState();       return;     }     alright.currentTime = details.seekTime;     updatePositionState();   }],   ['stop', () => {     alright.pause();     alright.currentTime = 0;   }], ]   if ( 'mediaSession' in navigator ) {   navigator.mediaSession.metadata = new MediaMetadata({     title: 'Alright',     artist: 'Kendrick Lamar',     album: 'To Pimp A Butterfly',     artwork: [       { src: 'https://mytechnicalarticle/kendrick-lamar/to-pimp-a-butterfly/alright/96x96', sizes: '96x96', type: 'image/png' },       { src: 'https://mytechnicalarticle/kendrick-lamar/to-pimp-a-butterfly/alright/128x128', sizes: '128x128', type: 'image/png' },       // More sizes, like 192x192, 256x256, 384x384, and 512x512     ]   });     for (const [action, handler] of actionsAndHandlers) {     try {       navigator.mediaSession.setActionHandler(action, handler);     } catch (error) {       console.log(`The media session action, $ {action}, is not supported`);     }   } }

Here’s a demo of the API:

I implemented six of the actions. Feel free to try the rest during your leisure.

If you view the Pen on your mobile device, notice how it appears on your notification area.

If your smart watch is paired to your device, take a sneak peek at it.

If you view the Pen on Chrome on desktop, navigate to the media hub and play with the media buttons there. The demo even has multiple tracks, so you experiment moving forward/back through tracks.

If you made it this far (or not), thanks for reading and please, on the next app you create with media functionality, implement this API.

The post Give Users Control: The Media Session API appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , , ,

One Way to Break Users Out of the Habit of Reloading Too Much

Page reloads are a thing. Sometimes we refresh a page when we think it’s unresponsive, or believe that new content is available. Sometimes we’re just mad at the dang site and rage-refresh to let it know we’re displeased.

Wouldn’t be nice to know when a user refreshes the page? Not just that, but how many times? That data can help us trigger some sort of behavior after a certain number of reloads.

A sports site is a good example. If I want to check the score of a game that’s in progress but the scores aren’t live-updated, then I might find myself refreshing a bunch.

Our goal is to break users out of that habit. We’ll use our page-refresh-counting powers to let folks know that refreshes are unnecessary, thanks to real-time score updates. And if they reload more than three times? We’ll kick ‘em out of their session. That’ll show them.

Here’s a simple demo of that concept.

Let’s re-create it together. But before we get going, there are few questions we need to answer before we start coding:

  • How can we persist the number of times user reloaded the site? We need a place to keep the number of times user reloaded the site (reloadCount), this place needs to persist that value between the reloads — localStorage sounds like a good solution.
  • How do we detect if user reloaded the site or just came back after few hours? If we store the reloadCount in localStorage it will persist the value between the reloads, but it will keep that value until we remove programmatically or clear the browser storage. It means that if we come back after few hours the site will still remember last reloadCount and may perform logout after first refresh without warning. We want to avoid that and allow user to reload the site two times each time the user comes back after some period of time. That last sentence holds the answer to the question. We need to store the time when the user left the site and then when the site loads again check when that happened. If that time period wasn’t long enough, we activate the reload counting logic.
  • How do we know when the user leaves the site? To store that time, we use beforeunload window event and store that value in localStorage.

OK, now that we have the answers, let’s dive into the code.

Step 1: We’ve gotta store the last reload time

We will store the time of last reload using a beforeunload window event. We need two things: (1) an event listener that will listen to the event and fire the appropriate method, and (2) our beforeUnloadHandler method.

First, let’s create a function called initializeReloadCount that will set our event listener using the addEventListener method on the window object.

function initializeReloadCount() {   window.addEventListener("beforeunload", beforeUnloadHandler) }

Then we create a second method that will be fired before we leave the site. This method will save the time the refresh happens in localStorage.

function beforeUnloadHandler() {   localStorage.setItem("lastUnloadAt", Math.floor(Date.now() / 1000))   window.removeEventListener("beforeunload", beforeUnloadHandler); }

Step 2: We need a way to handle and store the reload count

Now that we have the time when the site was last closed, we can proceed and implement logic that’s responsible for detecting and counting how many times the site was reloaded. We need a variable to hold our reloadCount and tell us how many times user reloaded the site.

let reloadCount = null

Then, in our initializeReloadCount function, we need to do two things:

  1. Check if we already have a reloadCount value stored in our localStorage, and if so, get that value and save it in our reloadCount. If the value doesn’t exist, it means that the user loaded the site for the first time (or at least did not reload it). In that case, we set the reloadCount to zero and save that value to localStorage.
  2. Detect if the site was reloaded or the user came back to the site after longer period of time. This is the place where we need our lastUnloadAt value. To detect if the site was actually reloaded, we need to compare the time when the site gets loaded (the current time) with the lastUnloadAt value. If those two happened within, say, five seconds (which is totally arbitrary), that means the user reloaded the site and we should run reload count logic. If the time period between those two events is longer, we reset the reloadCount value.

With that, let’s create a new function called checkReload and keep that logic there.

function checkReload() {   if (localStorage.getItem("reloadCount")) {     reloadCount = parseInt(localStorage.getItem("reloadCount"))   } else {     reloadCount = 0     localStorage.setItem("reloadCount", reloadCount)   }   if (     Math.floor(Date.now() / 1000) - localStorage.getItem("lastUnloadAt") <     5   ) {     onReloadDetected()   } else {     reloadCount = 0;     localStorage.setItem("reloadCount", reloadCount)   } }

The last function we need in this step is a method responsible for what happens when we confirm that the user reloaded the site. We call that function onReloadDetected, and inside it, we increment the value of reloadCount. If the user refreshed the site third time, we drop the bomb and call our logout logic.

function onReloadDetected() {   reloadCount = reloadCount + 1   localStorage.setItem("reloadCount", reloadCount)   if (reloadCount === 3) {     logout()   } }

Step 3: “Dear user, why you didn’t listen?!”

In this step, we implement the logic responsible for the situation when the user reloads the site to the point of breaching our three-limit threshold, despite our clear warnings to stop doing it.

When that happens, we call our API to log the user out, then we clean up all properties related to the reload count logic. That will allow the user to come back and have a clean account of reloads. We can also redirect the user somewhere useful, like the login screen. (But wouldn’t it be funny to send them here instead?)

function logout(params) {   // logout API call   resetReloadCount() }  function resetReloadCount() {   window.removeEventListener("beforeunload", beforeUnloadHandler)   localStorage.removeItem("lastUnloadAt")   localStorage.removeItem("reloadCount"); }

Bonus: Let’s re-Vue it!

Now that we have the logic implemented, let’s see how can move that logic to a Vue site based on this example:

First, we need to move all of our variables into our component’s data, which is where all reactive props live.

export default {   data() {     return {       reloadCount: 0,       warningMessages: [...]     }   },

Then we move all our functions to methods.

// ...   methods: {     beforeUnloadHandler() {...},     checkReload() {...},     logout() {...},     onReloadDetected() {...},     resetReloadCount() {...},     initializeReloadCount() {...}   } // ...

Since we are using Vue and its reactivity system, we can drop all direct DOM manipulations (e.g. document.getElementById("app").innerHTML) and depend on our warningMessages data property. To display the proper warning message we need to add a computed property that will re-calculate each time our reloadCount is changed so that we can return a string from our warningMessages.

computed: {   warningMessage() {     return this.warningMessages[this.reloadCount];   } },

Then we can access our computed property directly in the component’s template.

<template>   <div id="app">     <p>{{ warningMessage }}</p>   </div> </template>

Last thing we need to do is find a proper place to activate the reload prevention logic. Vue comes with component lifecycle hooks that are exactly what we need, specifically the created hook. Let’s drop that in.

// ...   created() {     this.initializeReloadCount();   }, // ...


Wrapping up

And there it is, the logic that checks and counts how many times a page has been refreshed. I hope you enjoyed the ride and you find this solution useful or at least inspiring to do something better. 🙂

The post One Way to Break Users Out of the Habit of Reloading Too Much appeared first on CSS-Tricks.


, , , ,

Detecting Inactive Users

Most of the time you don’t really care about whether a user is actively engaged or temporarily inactive on your application. Inactive, meaning, perhaps they got up to get a drink of water, or more likely, changed tabs to do something else for a bit. There are situations, though, when tracking the user activity and detecting inactive-ness might be handy.

Let’s think about few examples when you just might need that functionality:

  • tracking article reading time
  • auto saving form or document
  • auto pausing game
  • hiding video player controls
  • auto logging out users for security reasons

I recently encountered a feature that involved that last example, auto logging out inactive users for security reasons.

Why should we care about auto logout?

Many applications give users access to some amount of their personal data. Depending on the purpose of the application, the amount and the value of that data may be different. It may only be user’s name, but it may also be more sensitive data, like medical records, financial records, etc.

There are chances that some users may forget to log out and leave the session open. How many times has it happened to you? Maybe your phone suddenly rang, or you needed to leave immediately, leaving the browser on. Leaving a user session open is dangerous as someone else may use that session to extract sensitive data.

One way to fight this issue involves tracking if the user has interacted with the app within a certain period of time, then trigger logout if that time is exceeded. You may want to show a popover, or perhaps a timer that warns the user that logout is about to happen. Or you may just logout immediately when inactive user is detected.

Going one level down, what we want to do is count the time that’s passed from the user’s last interaction. If that time period is longer than our threshold, we want to fire our inactivity handler. If the user performs an action before the threshold is breached, we reset the counter and start counting again.

This article will show how we can implement such an activity tracking logic based on this example.

Step 1: Implement tracking logic

Let’s implement two functions. The first will be responsible for resetting our timer each time the user interacts with the app, and the second will handle situation when the user becomes inactive:

  • resetUserActivityTimeout – This will be our method that’s responsible for clearing the existing timeout and starting a new one each time the user interacts with the application.
  • inactiveUserAction – This will be our method that is fired when the user activity timeout runs out.
let userActivityTimeout = null;  function resetUserActivityTimeout() {   clearTimeout(userActivityTimeout);   userActivityTimeout = setTimeout(() => {     inactiveUserAction();   }, INACTIVE_USER_TIME_THRESHOLD); }  function inactiveUserAction() {   // logout logic }

OK, so we have methods responsible for tracking the activity but we do not use them anywhere yet.

Step 2: Tracking activation

Now we need to implement methods that are responsible for activating the tracking. In those methods, we add event listeners that will call our resetUserActivityTimeout method when the event is detected. You can listen on as many events as you want, but for simplicity, we will restrict that list to a few of the most common ones.

function activateActivityTracker() {   window.addEventListener("mousemove", resetUserActivityTimeout);   window.addEventListener("scroll", resetUserActivityTimeout);   window.addEventListener("keydown", resetUserActivityTimeout);   window.addEventListener("resize", resetUserActivityTimeout); }

That’s it. Our user tracking is ready. The only thing we need to do is to call the activateActivityTracker on our page load.

We can leave it like this, but if you look closer, there is a serious performance issue with the code we just committed. Each time the user interacts with the app, the whole logic runs. That’s good, but look closer. There are some types of events that are fired an enormous amount of times when the user interacts with the page, even if it isn’t necessary for our tracking. Let’s look at mousemove event. Even if you move your mouse just a touch, mousemove event will be fired dozens of times. This is a real performance killer. We can deal with that issue by introducing a throttler that will allow the user activity logic to be fired only once per specified time period.

Let’s do that now.

Step 3: Improve performance

First, we need to add one more variable that will keep reference to our throttler timeout.

let userActivityThrottlerTimeout = null

Then, we create a method that will create our throttler. In that method, we check if the throttler timeout already exists, and if it doesn’t, we create one that will fire the resetUserActivityTimeout after specific period of time. That is the period for which all user activity will not trigger the tracking logic again. After that time the throttler timeout is cleared allowing the next interaction to reset the activity tracker.

userActivityThrottler() {   if (!userActivityThrottlerTimeout) {     userActivityThrottlerTimeout = setTimeout(() => {       resetUserActivityTimeout();        clearTimeout(userActivityThrottlerTimeout);       userActivityThrottlerTimeout = null;     }, USER_ACTIVITY_THROTTLER_TIME);   } }

We just created a new method that should be fired on user interaction, so we need to remember to change the event handlers from resetUserActivityTimeout to userActivityThrottler in our activate logic.

activateActivityTracker() {   window.addEventListener("mousemove", userActivityThrottler);   // ... }

Bonus: Let’s reVue it!

Now that we have our activity tracking logic implemented let’s see how can move that logic to an application build with Vue. We will base the explanation on this example.

First we need to move all variables into our component’s data, that is the place where all reactive props live.

export default {   data() {     return {       isInactive: false,       userActivityThrottlerTimeout: null,       userActivityTimeout: null     };   }, // ...

Then we move all our functions to methods:

// ...   methods: {     activateActivityTracker() {...},     resetUserActivityTimeout() {...},     userActivityThrottler() {...},     inactiveUserAction() {...}   }, // ...

Since we are using Vue and it’s reactive system, we can drop all direct DOM manipulations i.(i.e. document.getElementById("app").innerHTML) and depend on our isInactive data property. We can access the data property directly in our component’s template like below.

<template>   <div id="app">     <p>User is inactive = {{ isInactive }}</p>   </div> </template>

Last thing we need to do is to find a proper place to activate the tracking logic. Vue comes with component lifecycle hooks which are exactly what we need — specifically the beforeMount hook. So let’s put it there.

// ...   beforeMount() {     this.activateActivityTracker();   }, // ...

There is one more thing we can do. Since we are using timeouts and register event listeners on window, it is always a good practice to clean up a little bit after ourselves. We can do that in another lifecycle hook, beforeDestroy. Let’s remove all listeners that we registered and clear all timeouts when the component’s lifecycle comes to an end.

// ...   beforeDestroy() {     window.removeEventListener("mousemove", this.userActivityThrottler);     window.removeEventListener("scroll", this.userActivityThrottler);     window.removeEventListener("keydown", this.userActivityThrottler);     window.removeEventListener("resize", this.userActivityThrottler);        clearTimeout(this.userActivityTimeout);     clearTimeout(this.userActivityThrottlerTimeout);   } // ...

That’s a wrap!

This example concentrates purely on detecting user interaction with the application, reacting to it and firing a method when no interaction is detected within specific period of time. I wanted this example to be as universal as possible, so that’s why I leave the implementation of what should happened when an inactive user it detected to you.

I hope you will find this solution useful in your project!

The post Detecting Inactive Users appeared first on CSS-Tricks.


, ,

Optimizing Images for Users with Slow Network Speeds

For every website, page load time is a critical factor that can make or break the business. Thanks to the better user experience that comes with a fast-loading webpage, those who focus on page load optimization enjoy better conversion rates, better SEO, better retention, and lower bounce rates.

And this fact has been highlighted in several studies. For example, according to one of the studies, 47% of consumers expect a web page to load in 2 seconds or less. No wonder that developers across the globe focus a lot on improving the webpage load time.

Logic dictates that keeping other factors the same, a lighter webpage should load faster than a heavier webpage, and that is the direction in which our webpages should head too. However, contrary to that logic, our websites have become heavier over the years. A look at data from HTTP Archive reveals that an average webpage in 2017 was almost three times heavier than what it used to be in 2011.

With more powerful user devices, faster networks, and the growing popularity of client-side frameworks and media-rich experiences, we have started pushing more and more data to the user’s device.

However, as developers, we miss a crucial point. We usually develop and test our websites in our offices over stable WiFi or wired connections. However, when a real-user uses our website, the network speed and stability may not be that great. Especially with more and more users coming online via mobile devices, the problem of fluctuating network conditions is even more significant.

Don’t believe it? ImageKit.io conducted a study to determine the network speed reported by the Network Info API of Chrome browser for users of a website (with visitors mostly from India). It is not very surprising that almost 40% of the visitors tracked had reported speed lower than 4G, i.e., less than 700 Kbps as per the Network Info API Spec.

While this percentage of users experiencing poor network conditions might be lower if we get visitors from developed countries like the USA or those in Europe, we can still safely assume that varying network conditions impact a sizeable chunk of our users. And we have all experienced it as well. When we enter an elevator or move to the basement parking lot of a building or travel to a remote location, the mobile data download speeds drop significantly. 

Therefore, we need to keep in mind that our users, especially the ones on mobile, will invariably try to visit our website on a slow network, and our goal as a developer should be to provide them with at least a decent browsing experience.

Why optimize images for slow networks?

The ultimate goal of optimizing a website for slower networks is to be able to serve its lighter variant. This way, all the essential stuff gets downloaded and displayed quickly on the user’s device. 

Amongst all the resources that load on a webpage, images make up for most of the payload. And even if we do take care of optimizing the images in general, optimizing them further for slower networks can reduce the overall page weight by almost 30%. 

Also, additional compression of images doesn’t break the critical functionality of any application. Yes, the image quality drops a bit to provide for better user experience. But unlike stripping away Javascript, which would require a lot of thought, compressing images is relatively straightforward.

How to optimize images for a slow network?

Now that we have established that optimizing our webpage based on the user’s network speed is essential and that images are the lowest-hanging fruit to get started, let’s look at how we can achieve network-based image optimization.

There are two parts of the solution.

Determine the user’s network speed

We need to determine the network speed that the user is experiencing and divide them into buckets. For example, users experiencing speed above a certain threshold should be classified in a single group and served a particular quality level of an image. This classification is simple in modern web browsers with the Network Information API. This API automatically classifies the users into four buckets – 4G, 3G, 2G, and slow 2G, with 4G being the fastest and slow 2G being the slowest. 

// returns '4g', '3g', '2g' or 'slow-2g' var effectiveType = NetworkInformation.effectiveType;

Compress the images to an appropriate quality level

The second part of the solution is to be able to alter the compression level of an image in real-time, depending on the user’s network speed determined in step 1. It should be as simple as passing an additional parameter in the image URL when the browser triggers a load for it.

While we rely on the browser to determine the user’s network speed, a tool like ImageKit.io makes the real-time compression bit simple. 

ImageKit.io is a real-time image optimization and transformation product that helps us deliver images in the right format, change compression levels, resize, crop, and transform images directly from the URL and deliver those images via a global image CDN. We can get the image with the desired compression level by just passing the image quality parameter in the URL. Quality is directly proportional to image size, i.e., higher the quality number, larger will be the resulting image.

// ImageKit URL with quality 90 https://ik.imagekit.io/demo/default-image.jpg?tr=q-90  // ImageKit URL with quality 50 https://ik.imagekit.io/demo/default-image.jpg?tr=q-50

How else does ImageKit help with network-based image optimization?

While ImageKit has always supported real-time URL-based image quality modification, it has started supporting the network-based image optimization features recently. With these new features, it has become effortless to implement complete network-based optimization with minimum effort.

Of course, first, we need to sign up for ImageKit and start delivering the images on our website through it. Once this is done, in the ImageKit dashboard, we have to enable the setting for network-based image optimization. We get a code snippet right there that and add it to an existing service worker on our website or to a new service worker. 

// Adding the code snippet in a service worker importScripts("https://runtime.imagekit.io/<your_imagekit_id>/v1/js/network-based-adaption.js?v=" + new Date().getTime());

Within the dashboard itself, we also need to specify the desired quality level for different network speed buckets. For example, we have used a quality of 90 for images for users classified as 4G users and a quality of 50 for our slow 2G users. Remember that lower quality results in smaller image sizes.

This code snippet is like a plugin meant for use in service workers. It intercepts the image requests originating from the user’s browser, detects the user’s network speed, and adds the necessary parameters to the image URL. These parameters can be understood by the ImageKit server to compress the image to the desired level and maintain efficient client-side caching. For network-based image optimization, all that we need to do is to include it on our website and done!

Additionally, in the ImageKit dashboard, we can specify the image URLs (or patterns in URLs) that should not be optimized based on the network type. For example, we would want to present the same crisp logo of our brand to our users regardless of their network speed.

Verifying the setup

Once set up correctly, we can quickly check if the network-based optimization is working using Chrome Developer Tools. We can emulate a slow network on our browser using the developer tools. If set up correctly, the service worker should add some parameters to the original image request indicating the current network speed. These parameters are understood by ImageKit’s servers to compress the image as per the quality settings specified in our ImageKit dashboard corresponding to that network speed.

How does caching work with the service worker in place?

ImageKit’s service worker plugin by-passes the browser cache in favor of network-based image cache in the browser. By-passing the browser cache means that the service worker can maintain different caches for different network types and choose the correct image from the cache or request a new one based on the user’s current network condition. 

The service worker plugin automatically uses the cache-first technique to load the images and also implements a waterfall method to pick the right one from the cache. With this waterfall method, images at higher quality get preference over images at a lower quality. What it means is that, if the user’s speed drops to 2G and he has a particular image cached from the time when he was experiencing good download speed on a 4G network, the service worker will use that cached 4G image for delivery instead of downloading the image over the 2G network. But the reverse is not valid. If the user is experiencing 4G network speeds, the service worker won’t pick up the 2G image from the cache, because it is possible to fetch a better quality image and the resources allow for it.

And there is more!

Apart from a simple, ready-to-use service worker plugin and dashboard settings, ImageKit has a few more things to offer that make it an attractive tool to get started with network-based optimization.


ImageKit provides us with analytics on our user’s observed network type. It gives us an insight into the number of times network-based optimization gets triggered and what does the user distribution look like across different network types. This distribution analysis can be helpful even for optimizing other resources on our website.

Cost of optimization

With network-based image optimizations, the size of the images goes down, but at the same time, the number of transformation requests can potentially go up. Unlike a lot of other real-time image optimization products out there, ImageKit’s pricing is based only on the output image bandwidth and nothing else. So with the favorable pricing model, implementing network-based image optimization not only provides a lot more value to our users but also helps us cut down on image delivery costs.


Improving page load performance is essential. However, there is one bit that we have all been missing – optimizing it for slow networks.

Images present an easy start when it comes to optimizing our entire website for different networks. With the support of network-based optimization features via service workers, it has become effortless to achieve it with ImageKit. 

It will be a great value add for our users and will help to improve the user experience even further, which will have a positive impact on the conversions on our website. 

Sign up now for ImageKit and get started with it now!

The post Optimizing Images for Users with Slow Network Speeds appeared first on CSS-Tricks.


, , , , ,