DEV Community

Cover image for How to achieve top-notch scrolling performance using HTML5 Canvas
Nick for SIP3

Posted on

How to achieve top-notch scrolling performance using HTML5 Canvas

Intro

Nowadays, more and more companies adopt Canvas API to implement new shiny things inside a web browser.
Recently Google announced that Google Docs will now use a canvas based rendering. The SIP3 team decided to keep up as well.

Problem

If you use canvas for something ambitious, there is a great chance that you will run into performance issues. I bet you are wondering how I knew. Turns out large canvases are slow, especially on energy-efficient Safari, and may even cause significant energy or significant memory alerts.

That’s exactly the issue we faced while working on the highly interactive call flow interface at SIP3.

Call Flow

Solution

So we end up creating our personal set of rules to effectively address canvas performance challenges.

1. Do not work with Canvas API directly

Either you use a frontend framework or vanilla JS/TS take a look at one of these wonderful libraries.
Our choice is awesome Konva.js with react-konva bindings. It abstracts away low-level details, like redrawing, bubbling or layering inside canvas.
Konva supports many useful shapes like Rect, Circle, Text, Arrow, that you can use out of the box to construct UI.
So instead of imperative low-level code:



const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

ctx.fillStyle = 'coral';
ctx.fillRect(0, 0, 200, 200);


Enter fullscreen mode Exit fullscreen mode

You can use declarative React components.



<Rect x={0} y={0} width={200} height={200} fill='coral' />


Enter fullscreen mode Exit fullscreen mode

Konva has built-in events support, through React-style onEventName API.



<Text x={0} y={0} width={50} fontSize={10} text='Click me' onClick={() => console.log('You clicked me')} />


Enter fullscreen mode Exit fullscreen mode

Also Konva supports components nesting, like regular React components.



<Layer>
    <Text text='Text on canvas' fontSize={15} />
    <Group>
        <Circle x={200} y={100} radius={50} fill='green' />
        <Line x={20} y={200} points={[0, 0, 100, 0, 100, 100]} closed stroke='black' />
    </Group>
</Layer>


Enter fullscreen mode Exit fullscreen mode

2. Do not make your canvas too big

Try to avoid creating a canvas that is larger than your screen, because anything more than that will probably drop performance in the long run.



<Stage width={window.innerWidth} height={window.innerHeight}>
  <Layer>
    {/* Your shapes here are at most the size of your screen */}
  </Layer>
</Stage>


Enter fullscreen mode Exit fullscreen mode

Restricting stage size offers great performance gains because browser engine does not move large amounts of bytes from memory to screen on each redraw. Check out the Stage Section in Performance tips list for more detailed explanation.

3. Do not redraw static shapes

Create a separate canvas, where all static shapes would be drawn and place this canvas underneath the main one.



<Stage>
  {/* Layer for static shapes (e.g. background) */}
  <Layer>
    <Rect />
  </Layer>

  {/* Layer for interactive shapes, that react to user interactions */}
  <Layer>
    <Text />
    <Line />
    <Circle />
  </Layer>
</Stage>


Enter fullscreen mode Exit fullscreen mode

It massively improves performance, because Konva internally creates separate canvas for each Layer and refreshes only a layer that changed. Check out the Layer Management section in the docs for more info.

4. Do not rely on native scrolling!

Emulate the scrollbars in case you need a scene bigger than your canvas real size.

This rule is probably the most important out there, just look at the examples.

The first one uses native scrolling and causes the performance lag on every scroll event.


However, the identical app with emulated scrollbars works perfectly fine.

Instead of conclusion

I guess the canvas-based rendering will spread in the nearest future. Lots of web-applications will eventually switch from the good old HTML elements to painting pixels on the canvas.

Some tools, like Flutter are already rendering all user interface inside canvas.

So the earlier you accept this trend the better. And I just hope these tips will save you time when profiling and tweaking canvas performance.

Happy canvasing...

Top comments (9)

Collapse
 
grahamthedev profile image
GrahamTheDev • Edited

So have you implemented a side-DOM for accessibility that replicates everything within the canvas like Google did?

It seems like an awful lot of extra work having to build things twice, what was the driving force behind going canvas based?

For example, in the emulated sidebars example, I can't scroll with arrow keys on your canvas version? That makes me quite worried that you haven't considered accessibility at all! 😨

Oh and the second example stutters when I apply a 6x CPU slowdown, the first example doesn't so I am not sure what the performance benefit is?

The scroll bars are also faulty, grab the very right hand side of the horizontal scroll bar and drag it left, the canvas stops rendering as you can drag beyond the bounds of the canvas. If you resize the page the scroll bars disappear entirely!

Sorry to sound negative but the example just doesn't work on any level, do you have a more complete example where all of these issues are addressed as it could just be the fiddle isn't complete enough?

Collapse
 
fromaline profile image
Nick

Hi, that's good point!

Canvas accessibility is one of the next steps. You are right, it's really a lot of extra work to add all necessary features, like arrow scrolling.
But the reality is that we need canvas for our call flow interface to completely decouple it from normal HTML flow and be able to move and place UI elements using сoordinate system.

Performance regarding we have a lot to do as well. But current production build is comfortable enough to be used even on mid-tier devices.

The example isn't good enough, I'll consider improving it.

Thanks for the detailed feedback ;)
Have a nice day!

Collapse
 
grahamthedev profile image
GrahamTheDev

I do not envy you! Understandable why you went canvas, the GIF was misleading as that looked very much like something that would be easier to build in HTML. But if you are building a flow manager UI...yeah, I guess you are stuck with the horrendous task of trying to make a canvas accessible!

Good luck with the project, I hope it all comes together!

Thread Thread
 
fromaline profile image
Nick

Thanks!

Collapse
 
andreevilya profile image
andreev-ilya

Wow! That has a potential to solve some problems with Canvas.
Good job;)

Collapse
 
jonrandy profile image
Jon Randy 🎖️ • Edited

With the two examples above, the scrolling and performance is massively better on the first one (buttery smooth) - not the second. Tried on both Firefox and Chrome

Collapse
 
yutamago profile image
Yutamago

Same for me. Although I have only tried it on my phone.
Plus, I can only scroll the second example by dragging the scrollbars, which is a bit annoying, while I can just drag the screen at the first example.

Collapse
 
fromaline profile image
Nick

Yeah, you are both right. I've added too synthetic examples due to a lack of experience in writing tech-related articles.
The real UI from out project freezes with real scrollbars especially on Safari, but works decent with emulated ones.
As I stated above, I'll consider improving the examples.
Thanks for the feedback!

Collapse
 
shaifaligoyal28 profile image
shaifaligoyal28

For One of my projects, I am using canvas directly without any of the libraries mentioned. the canvas is of the size of viewport and when it is zoomed, want to add scrollbars to it. What approach can be followed here. Please provide some help here. the scrollbars should only appear in case of zoom when canvas extends the viewport height or width.