Getting serious about SVG Performance, drawing, and more
iD depends on SVG for drawing map features, displaying tiles, and a model upon which to build complex interactions.
In the process of building it, we’ve learned a lot about SVG’s performance equation - and it’s time to share some of this. A lot of this is taken from NOTES.md, a sort of developer-journal which has grown over the last few weeks.
note that the examples in this post are designed to run around ~30fps in Google Chrome on a fast computer. They’re made to push the limits, so may crash slower browsers on slower machines.
First Steps with SVG
We’re generally using d3 for graphics in iD. It’s a very,
very light abstraction layer over SVG - it eliminates the need for manual
namespacing, gives a jQuery-like chainable
attr setter, and so on.
SVG elements are elements on-page just like HTML elements, with a few differences:
- There’s no
z-index- order in-page is the rendering model
- SVG has its own
transformproperty, which is different and has difference performance implications than CSS’s
- Many CSS styles work with SVG elements, but many do not. Some things we’d think of as styling variables, like the radii of circles, are actually expressed as attributes rather than style rules.
- SVG has a powerful and possibly awful system of doing inter-references: a
<defs>element for definitions which are used by other elements by
xlink:refattributes. For instance, you need to define a
<defs>in order to reference it from a
textPathelement in your main drawing canvas if you wish to draw text on a path.
There are a few basic goals of any performance tuning task:
- Reducing repaints & reflows
- Triggering code paths that your browser can optimize, like using rounded pixels in Canvas or using specific forms of functions to avoid V8 deoptimization
SVG’s special moves: g and defs
The basic structure of the map canvas for iD is as follows:
div (supersurface) svg (surface) defs rect#clip path (textPath data) g (tilegroup) r (vector root) g (fill, casing, stroke, text, hit, temp) (path, g, marker, etc)
Besides just having more shapes, SVG has different concepts - one is the
element, that represents a group. The groups can be very useful, since they
can be transformed with the
transform attribute: for instance, markers are
set up like
SVG’s defs element is another oddball:
it defines content that doesn’t show up on screen, but can be used via reference:
I’ll mention this later on with
textPath as a example. But another usage of defs
for this project is making a
rect element which is used to clip the vector
and tile layers.
CSS Transforms are a big boost - sometimes.
For panning the map, transforms seemed to be the main solution - they allow simple pan behaviors to be carried out on a single element, and in a way that doesn’t require a browser to recalculate individual element positions.
However, early results were not encouraging - CSS transforms, even of the ‘hardware accelerated’ 3D variety - are not accelerated in Google Chrome when used with SVG elements - they have the same performance profile as SVG attribute transforms.
The answer is to transform HTML elements, which does trigger fast code in
Google Chrome. In iD we do this by transforming a ‘surface’ element (the SVG parent)
of the map instead of the group (
<g>) elements that make up the tile surface
and vector surface.
- CSS transforms on an SVG element with many contained elements
- CSS 3d transforms on an HTML element with many contained elements
(update: these now compensate for different vendor prefixes for the
Rounded pixels give a boost
Vladimir brought up this one and I didn’t expect
it to be a significant bump, since SVG is vectorized, I assumed that it wouldn’t
have raster-pixel bumps like Canvas does. However, it is a boost, and not
just because of the shorter string length of
d attributes - rounding and
outputting long fixed-point numbers is also faster than non-rounded values.
This is about a 20% improvement in testing.
Markers along a path
One of the features we need in iD is an indication of roads that are one-way, and which way they are going.
SVG has some built-in functionality for this purpose - you can define a
marker in a
defs element and use it on
path elements with
and so on. Unfortunately, this doesn’t cut it, since OSM paths have an extremely
variable number of vertices - some straight lines have hundreds of vertices,
some have only a few, and would be very sparsely labeled.
Our current solution to this is to use a
textPath with a glyph - a right-facing
triangle. Here’s what it looks like in a standalone demo.
First, determine the size of an arrow - how many pixels it occupies from left to right on a path:
getTotalLength lets you get the pixel-length of a
path, and you can
then use multiplication & letter spacing to distribute just enough markers
for the path:
Update: I noticed that this example is broken in Firefox. This is due to the following (also noted on the example page):
The original version of this failed because Firefox does not support letter-spacing on SVG elements. The second version failed because it has a bug in getComputedTextLength. The third failed because it does not preserve space according to xml:space.
Needless to say, Firefox is not the main target for iD testing because of a buggy and incomplete implementation of SVG relative to WebKit.
Devlogging work on the OpenStreetMap project by the MapBox team.
Much of this work is currently focused on improvements to OpenStreetMap funded by the Knight Foundation