At some level of UI complexity, most developers start wondering whether they’re better off rendering their Scene using a JavaFX Canvas object, rather than loading everything in as a Shape on a layout Pane.

Especially if your UI is starting to get laggy or unresponsive.

When you load a Shape or a Node onto a Pane, JavaFX maintains dozens of properties associated with the shape’s bounds, transforms and style properties. That creates an overhead as your scene gets more complicated.

It’s really convenient to be able to access all of those, but at what cost?

The canvas trades all of that convenience for the performance benefits basically painting everything as a single flattened, 2D texture.

So, if you’re noticing some lagging in your UI and you’re trying to improve performance, using a Canvas might be the way to go.

How do I choose between them then?!

Actually working out when you need to switch between using a Pane and a Canvas can be really tricky. To test each, I designed three challenging use cases to test the performance Canvas and the Pane in varying combinations of computational and rendering requirements.

Official nameDescriptionComputation between framesUI Frame Rate test:
Object TransformsCreate a bunch of objects and move them around a bunch and see how the UI deals with it.LowEvery pixel is drawn several times (many layers of objects) per frame.
Collision DetectionCreate slightly fewer objects, but make them test whether they intersect with other objects.ModerateEvery pixel is drawn at least once.
Gravity swarmCreate a lot of objects – I’m talking millions.

Apply forces to every object, like gravity.
HighMost pixels don’t need to be drawn every frame.

Memory usage: As a bonus, because I’m creating so many objects, these tests also make a really good test for how memory-efficient the Pane and Canvas are.

A sneak peak at the results

In every performance test, the Canvas renders objects more efficiently, requiring less memory and outperforming the Pane when the scene becomes too complex. At high computational complexity, the Canvas object also throttles the refresh rate, keeping the frame rate consistent at 30 FPS for longer.

But that’s not to say the Canvas is always better. Both the Canvas and the Pane objects, and their sister objects in the Prism rendering layer, maintain knowledge of which areas of their node have been modified and need to be recomputed for rendering. That means if your UI is quite simple, JavaFX won’t waste performance on recomputing the contents of your scene every frame.

On computers with a reasonable processor, you may have to make a scene very complex (usually tens of thousands of objects) before a difference between the Canvas and Pane becomes noticeable.

Whether to use a Pane or a Canvas depends completely on what you want to use them for, how much convenience you need and – sometimes – on where you think your users are going to deploy your program.

How the tests work:

The tests are designed so that the Canvas and Pane attempt to render the same Scene, but produced in different ways.

Object Transforms

Collision Detection

Gravity swarm

Each test will measure the frame rate of the Canvas and Pane objects. The tests increase in complexity from a simple scene (where neither Canvas nor Pane struggle) to a complex one (where both struggle).

In each case, I’ll also compare the length of code required to generate the sample.

Alright then, on we go!

Test 1: Object transforms

This first test is all about what it looks like when your scene gets too complex. You might not be doing any computation, but you’ve just gone and added too many elements.

To give you an idea of just how many, the test varies from 4,000 to 400K.

There’s this idea floating around in JavaFX forums that if you’re using shapes in your user interface, it’s much more convenient to use a Pane because all of your transforms (like translation and rotation) are already baked into the Shape objects you’re using.

The Canvas itself has an amazing array of methods to draw basic (and complex) shapes, so as long as you’re happy creating an object to store their position, you’re basically golden.

Test description:

In this test, I have literally just created a pre-defined number of circles, in exactly the same way (radius 10px, random color) in both a Canvas and Pane. Then, I give every particle a random movement vector and update their position every frame.

Simple object transforms are used to test rendering in a canvas and pane node

Obviously, I’ll use a slightly bigger window and more circles, but bear with me.

OK this looks good…

Code length:

What I’m interested in testing here is the idea that using a Pane is much more concise in code. The Canvas has a lot of built in methods to draw circles, rectangles and SVG images, which we’ll also make use of.

Head-to-head Comparison

Pane
Create objects:13
Animation:13
Particle:82
Total:117
Canvas
Create objects:14
Animation:16
Particle:89
Total:124

Summary:

Overall, there’s almost no benefit to using the Pane in terms of code length, because the majority of the complexity comes from the need to create custom behaviour for our particles.

Each particle (Canvas and Pane) needs to have the capacity to calculate attraction and movement in addition to position, as well as to bounce elastically off vertical and horizontal boundaries.

In this case, the particle extends the Circle Shape object. That allows us to keep the convenience of the Circle shape, but add custom behaviour.

The only ‘extra’ properties our Canvas Particle needs compared to the Pane Particle are its position, and a Color, because the Circle object contains both position and color already, so it’s not needed for our Pane Particle. If we were creating more complex shapes with transforms, these would need to be added too.

That’s the benefit of using a custom object though – only adding the properties you need. We have no use for rotation or scale here, so it’s not necessary to include them.

Code efficiency

OK, this is probably what you’ve been waiting for. Basically I’m going to ramp up the number of circles until the Canvas and Pane start to struggle. JavaFX isn’t necessarily smart enough to know that layers of objects can’t be seen, so it tries to render everything I’m giving it.

I ran the test between 4K and 400K circles, at which point both the Canvas and Pane started to struggle quite a lot. I’m running these on a i7-1065G7 processor, which is OK but by no means award winning.

Results:

Below 10,000 nodes (green), JavaFX’s artificial framerate limiter means you can’t notice the difference between the Canvas and Pane objects. However, above 10,000 (yellow) the Pane begins to struggle.

Interestingly, the Pane object doesn’t outsource as much of its rendering to the GPU, so even when the limitation is to do with rendering, changes to a Pane are calculated and rendered mostly on the CPU.

Looking at the graph, between 25,000 and 100,000 actually looks really interesting. While the Pane continues to struggle, the Canvas seems to be able to compensate for heavy render load in ways the Pane can’t.

What’s interesting is that the limiting factor here is rendering performance. The actual calculation-based CPU load is really low because all we’re doing is calculating a simple vector addition (we’re moving it, but by the same amount every frame).

Somwhow, the Canvas’s internal optimisations means it stabilises at 30 FPS for a really significant amount of time. My suspicion is that this is something to do with the internal buffer processing being culled when it falls behind.

If you’re interested in finding out more about the Canvas, please check out my write up, which goes into a lot of detail.

A reset pulse is requested by the Canvas when the buffer processing is running behind

Test 2: Collision Detection

To ramp things up a little, we can increase the computational complexity a little by including the need to not only render objects, but also detect whether they collide with other objects on the screen.

Test description:

Here, I’ll render a background grid of a defined size, and create foreground objects, which will move across the grid. Where a foreground (coloured) object intersects with the grid, we’ll color the grid in the corresponding color.

Once this scene gets pretty congested, it’s actually fairly fun to look at.

Code length:

Again, to test the idea that code is more concise when using shapes on a pane, I looked at the number of lines of code required to generate the scene in each case.

Head-to-head Comparison

Pane
Create objects:27
Animation:26
Rectangle:0*
Total:53
*We’re using the Rectangle and Point2D objects, which come with JavaFX
Canvas
Create objects:24
Animation:27
Bounding box:20
Total:77

Unlike in the previous example, all of JavaFX’s Shape and Node objects have inbuild Bounds, which we can use to work out whether two objects are intersecting, but nothing we’ve created on the Canvas has that. For our Canvas implementation, we need to build in some of that functionality.

The cost is 20 lines of code. That gives us an intersects() method, and maintains a memory of where the object is, and its dimensions.

Code efficiency

Again, we’re comparing here the efficiency of each implementation in dealing with a scene that gets more and more packed. Specifically, what I’m doing is decreasing the grid size of the background, meaning each foreground rectangle has to calculate more and more intersections.

Looking at the graph, bear in mind the units are million collision calculations per second.

Results:

In this case, we actually don’t need to do much processing before the Pane begins to suffer.

Above about 800K per-frame collision-calculations, the achievable framerate for the Pane drops well below 30 FPS. Our Canvas implementation is stable until about 2.5 million.

Above 2.5 million, the Canvas begins to suffer too, but by this time the Pane is basically clawing its way to render hell, whatever that is.

Performance comparison between the Canvas and Pane objects for collision detection

I’ll just say again that this obviously depends on where you’re running your code. I’m running it on a i7-1065G7 processor. That’s lightyears ahead of a Raspberry Pi, but nowhere near most desktop processors.

Comments:

It can be more convenient to have JavaFX maintain the transforms applied to objects indefinitely. When arranging nodes on a Pane, each node comes with a Bounds object, which represents the location of the node in the parent Pane, as well as the wider Scene. The node also stores handy properties like opacity, which you can modify.

All of that means that you can automatically calculate intersection, move objects, resize them and perform transitions like fading.

800K collisions is quite a lot of collisions to have to detect in a frame… In fact, that’s 900 objects all detecting collisions between each other before you begin to notice the difference.

I prefer the Canvas when I’m making precise and controllable changes to objects, like during game development and custom charts / graphs. The canvas object gives you pixel-specific control and you get to choose which object properties you want to maintain.

You can also simplify or complicate collision detection to suit the accuracy you need. Whereas, with Shapes on a Pane, you are ‘stuck’ with the implementations provided.

Test 3: Gravity Swarm

Both of the above examples have demonstrated examples where near-enough every pixel is re-drawn every frame. This tested JavaFX’s ability to balance CPU and GPU load (basically only the Canvas takes advantage of this).

Changing every pixel multiple times per frame is relatively rare, I’d imagine.

So, in this example, well draw on a relatively small proportion of the canvas, but we’ll massively increase the computational complexity.

Test description:

One of the best ways to increase computational complexity is adding trigonometry, square roots, and normalisation. This way, you don’t need many points, because each frame every particle needs multiple, complex adjustments based on forces, position, and velocity.

For this example, I’ll draw each particle much smaller here (radius 2 px). That way, I can draw hundreds of thousands of particles but with a relatively small pixel footprint. In fact, the overall effect is going to be to produce a swarm that focusses around a single point.

As an added benefit, I think it looks pretty damn cool, which is always an indication it’s a well-designed test. Perhaps.

Code length:

In the final test, we’ll repeat the process of testing to what extent shapes on a pane are more concise. Here, we have custom behaviour that extends beyond what JavaFX provides with its default shapes, s we’ll need customisation in both cases.

Head-to-head Comparison

Pane
Create objects:6
Animation:13
Particle:82
Total:101
Canvas
Create objects:4
Animation:17
Bounding box:89
Total:110

In this case, it’s actually slightly cheaper to create the objects in our Canvas implementation, because we only need to add them to a list we’re maintaining. In the Pane implementation, we need to add them to a list, and to the Pane itself.

Other than that, the differences are relatively minor, with most behaviours being implemented in similar numbers of lines in each case. In the end, the Pane steals it by 9 lines.

Code efficiency

For a final time, let’s look at the gravity swarm as it gets bigger and bigger. In this case, both implementations hold up really well until about 10,000 particles in the swarm.

Above this threshold, the Pane begins to render more slowly, slowing down until just below 100K, at which point I stopped testing it.

Running on an i7-1065G7 processor, the Canvas doesn’t lose any pace below 40K, at which point it begins to fail at a similar rate to the Pane. I eventually stopped testing it at 400K.

Performance comparison between the Canvas and Pane objects in high-computation scenarios
This is drawn with a logarithmic scale (it gets big really fast towards the right).

I think generally for me this shows two things Firstly, you actually need quite a lot of nodes before your scene starts to slow down, but for the third time, the Canvas outperforms the Pane.

Memory efficiency:

One extra thing I haven’t mentioned, which you’re probably already thinking, is about memory efficiency. The key to this is that the Canvas flattens all of its drawn objects into a single texture. This is orders of magnitude more memory-efficient.

This is basically true generally across all of these tests, but more so here because the limitation is node number. It’s not on computational complexity, like in collision detection and gravity simulations.

Using the Pane method, Shape objects and their bounds are taking up 20% of the heap memory, which – by the time you get to 100,000 objects, comes to about 600 MB (that’s 120 MB for our shapes and bounds). The Prism rendering layer (which is JavaFX’s carbon-copy scene for rendering purposes) is taking up another 18%).

So, about 240MB for rendering…

Using the Canvas method, the heap is about half the size (even smaller with efficient garbage collection), at 150MB, and consists mostly of the byte arrays the Canvas uses to shuttle rendering instructions to the GPU. These account for about 30 MB. Our custom Particles and Points come to about 9 MB.

In total? 39 MB.

Conclusions:

Two major conclusions stand out to me from all of these tests:

  1. When you need performance, always go with the Canvas
  2. Code is not that much more concise with a Pane implementation (but it depends)

Here are some of my comments on each:

Performance:

In both memory and rendering performance (frame rate), the Canvas outstrips the Pane in every challenging scenario. In high CPU scenarios, the Canvas‘s more lightweight approach means it can process more objects more quickly.

On top of that, in any scenario with heavy GPU load, the Canvas also compensates for its inability to process all rendering instructions by ditching the rendering buffer when it’s running behind.

This means it maintains a frame rate of 30 FPS for even longer before it begins to struggle.

Code length:

Code doesn’t have to be that much longer when you’re implementing a Canvas-based solution to a busy UI.

But…

There are specific functionalities that every Node has that can be really useful:

  1. Calculating intersections
  2. Storying transforms like rotations and translations
  3. Animations (although not likely to be efficient)
  4. Mouse / Touch / Gesture events

One thing I did notice, however, was that once the UI starts to struggle, JavaFX starts trying to free up time to process its UI changes by batching up things like event handling.

That means if your UI is very busy, and you’re also trying to use event handling, this lagging will only be made worse .

Overall

At the end of the day, I think there are specific situations where you might want to use both of these objects. However, I’d reserve using a standard Shape/Node/Pane set up for what I’d call a “Standard” UI.

Dashboards. Music players. UIs where CSS styling is important.

Ultimately, I enjoy using a Canvas for games and custom implementations, more because it feels natural inside a game loop. But the Canvas can also be used to recreate many of the features of a busy UI with a smaller memory footprint and lower processor use.