• Stars
    star
    306
  • Rank 136,456 (Top 3 %)
  • Language
    JavaScript
  • Created over 8 years ago
  • Updated about 8 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Making a Doughnut Progress Bar - research notes

Making a Doughnut Progress Bar 🍩

Design

I was working on a new user profile component for my company. One of its elements is a combination of an avatar and a progress bar. First design that I got from our graphic design team looked like this:

design

Research

Rounded avatar and a white bubble in the background can be easily created with CSS. Progress bar is definitely the most challenging element. I considered three ways of building it:

  • CSS,
  • canvas and
  • SVG.

Looking through the different CSS pie chart implementations I decided that CSS is not fitted for the job. All solutions were hacks that would be hard to control. I was also worried that it will be cumbersome to make it work on all the browsers that we support. Canvas, with almost universal support, seemed more appealing. However, canvas would require me to implement timing (e.g. easing functions) and scaling (to support higher dpis) myself. Besides, I though that it would not utilize GPU as well as CSS transitions/transforms. SVG seemed like the best of both worlds: it's universally supported, doesn't need any hacks to create a required shape, utilizes CSS transitions to create animations and fits all screen dpis out of the box. So, I dusted off my SVG knowledge and started coding.

Prototype (master branch)

First prototype was ready in couple of hours and it looked like this:

prototype

Happy with the result, I sprinkled it with some CSS magic and made a short video showing it in action.

Everything, besides the progress bar, was done in HTML/CSS. Animation of the progress bar was powered by Jake Archibald's animated line drawing technique.

how it's built

On my way home from work I realized that I haven't yet looked at the performance of that component. I also remembered that during last Chrome Dev Summit someone mentioned that Chromes SVG implementation is dated and not especially performant. That made me fear for the worst. On the next day, I opened the prototype on my HTC One and, sure enough, saw a choppy animation. I debugged it remotely and got this timeline:

prototype - timeline

(For these unfamiliar with DevTools Timeline - red marks point out lost frames and the FPS graph should, ideally, be all green and flat.)

And so I started looking for optimizations.

Optimizations

Conic gradient (svg-image branch)

Progress bar is using conic gradient in the background. Since this type of gradient is not supported natively in SVG (nor CSS), I had to generate it myself (with SVG and JS). My solution turned out to be suboptimal. Using paint profiler I found out that gradient is regenerated in each frame (which is a lot of unnecessary work):

paint profiler FTW

My fix was to generate each background once and use animated SVG mask to reveal it.

    <svg>
      <defs>
          <mask id="progressPath">
            <path d="..."/>
          </mask>
      </defs>

      <image class="background" width="110" height="110" xlink:href="..." mask="url(#progressPath)"/>
    </svg>

This time I used Lea Verou's conic gradient polyfill to generate PNGs for me.

That helped a bit, but animation was still far from smooth (in fact, it was so far from smooth that android dev who walked by my desk haven't missed a chance to mock it). I decided to discuss it with my colleague who showed me how to get rid of masking (that we assumed was expensive).

Masking (reverse branch)

Instead of masking, he suggested putting gradient background on the element behind the progress bar. This required me to modify the SVG animation, but made the whole solution much simpler (compare image below with the similar image from the "prototype" section).

animation simplified

In the process I also learned that setting base64 background directly on the element using el.style.backgroundImage has poor performance (possibly due to parsing) so, instead, I generated CSS classes for each gradient on the fly.

Additionally, to reduce the time browser spends on painting, we forced it (via translateZ/will-change) to promote avatar and SVG to separate layers.

After all these optimizations (and numerous hours) the result was still rather disappointing:

loosing frames here and there

Two biggest offenders were rasterization and compositing.

Loosing faith in SVG (minimal branch)

I figured out that maybe it's all because I'm using web animation API? Or maybe because I use flexbox? Maybe because I'm doing two animations at once? Maybe it's all gradient's fault? So I started removing things one by one. Eventually, I ended up with 15 lines of HTML/SVG, 25 lines of CSS and no JS. That's how the simplified animation looked:

as simple as that

And that's a timeline of this, absolutely minimal, animation on my Android device:

SVG and animations - the sad truth

(β•―Β°β–‘Β°)β•―οΈ΅ ┻━┻

Canvas (canvas branch)

I quickly rebuilt the whole thing using canvas. There is much more JS magic going on, but rasterization is no longer an issue and animation feels waaay smoother. It still isn't perfect, compositing steals a frame once in a while, but I'm much closer to the final version.

canvas

Summary

I love SVG. It's elegant, scalable and works everywhere. It's perfect for mobile... as long as it doesn't move. There is no way to animate it smoothly on Android - rasterization won't give you a chance:

try to fit this in you ~16ms budget

So, why on the desktop even the unoptimized version of my animation, the prototype, works smoothly? That's simply because there are multiple rasterization threads that can handle the load:

rasterization on desktop

During last Chrome Dev Summit, Chrome team promised to step up their SVG game. I really hope that this will happen soon. In my opinion, SVG is a natural fit for creating complex animations on the web.

Meanwhile, I'll probably go with the canvas solution. It puts much more work on JS, but has a decent performance, is universally supported and, with a bit of work, I can make it look sharp on all dpis.

Follow-up

Paul Irish did a very insightful performance review of the SVG version of my animation. Unfortunately, he haven't found any significant improvements.


I made a safari & edge friendly version of the "reverse" branch (see reverse-es5), added FPSMeter and tested it side by side on Nexus 5X, iPhone5S, Lumia 735. Here is the result:

running side by side

It's hard to tell if this test is fair since these phones have completely different specs. On the other hand, these are all modern devices and run lastest versions of the browsers, so I'd expect all of them to show this animation at the smooth 60FPS.


Before making a final decision I decided to also test the CSS version of the animation (see css branch). As I thought, it turned out to be one big hack that's hard to work with. Also, since the implementation uses two separate elements that have to be synced, making non-linear easing would be challenging. Taking these issues into consideration, despite the fact that it was the most performant solution out of the three I tested, I made the decision to stick with canvas.


Based on the above research I created a simple library wrapping my canvas implementation: https://brainly.github.io/ui-components/components/doughnut-progress-bar/ . We ended up using it on production to create this little widget:

(almost) final product

More Repositories

1

betwixt

⚑ Web Debugging Proxy based on Chrome DevTools Network panel.
JavaScript
4,533
star
2

SnappySnippet

Chrome extension that allows easy extraction of CSS and HTML from selected element.
CSS
1,082
star
3

JS-OCR-demo

JavaScript optical character recognition demo
JavaScript
479
star
4

Proofreader

Simple text proofreader based on 'write-good' (hemingway-app-like suggestions) and 'nodehun' (spelling).
JavaScript
333
star
5

DOMListenerExtension

Chrome extension that allows you to monitor, browse and filter all DOM changes.
JavaScript
234
star
6

CSS-Diff

Chrome extension that allows to easily compare CSS of two HTML elements.
JavaScript
200
star
7

Context

Chrome extension that allows to sort other extensions into groups and easily switch between them.
JavaScript
173
star
8

JS-face-tracking-demo

JavaScript face tracking demo.
JavaScript
159
star
9

Look-alike

Visual regression testing tool.
JavaScript
33
star
10

Redmine-Issues-Checker

Chrome extension that helps you to keep track of your Redmine issues.
JavaScript
25
star
11

dailyjs-survey-sankey-diagrams

Daily JS survey results visualised with Sankey diagrams
CSS
23
star
12

DevToolsVoiceCommands

Experimental extension that allows inspecting and modifying websites using voice commands.
JavaScript
18
star
13

git-style-image-diff

Compare images line by line using git-like diffing algo
JavaScript
11
star
14

eZDebugHelper

Chrome extension for eZPublish developers.
JavaScript
8
star
15

AllGitHubTraffic

Plot data from all your GitHub repositories on one chart.
JavaScript
7
star
16

fp

Researching browser fingerprinting protection…
JavaScript
4
star
17

TheMoleGame

The Amazing Mole - JavaScript game for the js13kgames.com competition 2014
JavaScript
4
star
18

CriticalCSS

[WIP] chrome extension that offers one-click critical CSS extraction
JavaScript
4
star
19

web-privacy-pterodactyl

Web Privacy Pterodactyl disapproves trackers
4
star
20

jQueryAir

Looking for something to rewrite in your favourite MV* framework? Why not jQueryAir?
JavaScript
3
star
21

snailbook

The slooowest website on the Internet
JavaScript
3
star
22

MasonryTimeline

jQuery plugin - vertical list of posts with interactive timeline
JavaScript
3
star
23

GDEExtensionsWorkshop

Chrome Extensions Workshop for GDEs
3
star
24

MWPresentationTemplate

Making Waves presentation template based on html5slides project
JavaScript
2
star
25

schema-org-wip

JavaScript
2
star
26

fisheye-slow

Particularly bad implementation of the fisheye effect 🐌 For YOU to optimize πŸš€
JavaScript
2
star
27

HybridAppsPresentation

JavaScript
2
star
28

initiator-graph

Graph of top third party domains found on the web connected by their top initiators.
JavaScript
2
star
29

VersusExtension

Chrome extension that allows you to quickly compare number of Google results for multiple queries.
JavaScript
1
star
30

ChromeExtensionsWorkshop

Chrome Extensions Workshop Materials
1
star
31

TheBadLuckGame

JavaScript game for the js13kgames.com contest 2013
JavaScript
1
star
32

DevToolsPresentation

"5 common web development problems and how to solve them with Chrome Developer Tools"
1
star
33

MockieTalkie

JavaScript
1
star
34

CSS3AnimationPresentation

Lets get rid of GIFs and Flash!
JavaScript
1
star