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 name | Description | Computation between frames | UI Frame Rate test: |
---|---|---|---|
Object Transforms | Create a bunch of objects and move them around a bunch and see how the UI deals with it. | Low | Every pixel is drawn several times (many layers of objects) per frame. |
Collision Detection | Create slightly fewer objects, but make them test whether they intersect with other objects. | Moderate | Every pixel is drawn at least once. |
Gravity swarm | Create a lot of objects – I’m talking millions. Apply forces to every object, like gravity. | High | Most 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.

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.

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 |
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.

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.

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:
- When you need performance, always go with the
Canvas
- 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:
- Calculating intersections
- Storying transforms like rotations and translations
- Animations (although not likely to be efficient)
- 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.