Squeezing the most out of Raphaël JS for SVG generation

Squeezing the most out of Raphaël JS for SVG generation

A few hints about squeezing maximum performance out of Raphaël.

When you want to generate dynamic SVG images using Javascript, either on a website or within a Cordova-type project on mobile devices, the Raphaël library is pretty sweet.  Raphaël has a nice, clean, API and lets you do pretty much anything graphics-related and it sticks to the standards such that every graphical element is part of the DOM–so you can attach javascript event handlers and deal with them like any other element on the page.

One specific project I was working on involved graphing a lot of elements dynamically–essentially creating thousands of circles and paths to reflect the values of hardware sensors, both in real time and from large stored data-sets.

Here’s a short demo of a few preliminary versions of the code, in action in browser and on a tablet:


My major problem wasn’t with the function but with how well it was performing, with so many elements to create while trying to stay responsive to the user while also servicing the other demands on the software.  When I got down to profiling, I noticed that two Raphael functions were gobbling up almost 40% of my processor time:

    setFillAndStroke(); and

    clone();

Optimizing fill and stroke

A bit of investigating under the hood revealed one easy optimization.  Unlike the name would imply, setFillAndStroke() is a big function that handles a whole lot of parameters: x, y, height, blur, gradient, path, href, target, opacity, etc. etc. and of course stroke and fill.

Some of these parameters require a good deal of internal work to handle, while others can be handled in a very straight-forward fashion (depending on what you’re handing the function).

When you call, for instance:

    someElement.attr('fill', '#ff0000');

Raphaël will do some figuring and, eventually, wind up in setFillAndStroke(). After testing it’s parameters for all the stuff you aren’t doing, it will find it’s way to the place where it handles the fill and then massage you’re actual value (which could be a colour, gradient or image) into something it can grok.

Because, in the case above, the value is already nicely encoded as an HTML hex value, pretty much all this processing is pointless.  The down & dirty optimization is to replace:

    someElement.attr('fill', '#ff0000');

with

    someElement.node.setAttribute('fill', '#ff0000');

As soon as I did that, setFillAndStroke() disappeared from the top of my profiling list to somewhere far, far away.

Notes:
Obviously, this only works for attributes that wind up directly assigned to the node.  Some things just don’t work that way (like a circle radius), some are more complex (like RaphaelJS text elements) and some operations in setFillAndStroke() have secondary effects (like marking the object as dirty), so your mileage will definitely vary but if you’ve got a simple setting to apply to lots of elements, it may be worth exploring this option.

Also worthy of note is that this is totally undocumented.  It works fine with version 2.1.2… no guarantees it will down the line, though.

Attack of the clone()s

Why the clone() op was being called so much turned out to be my use of paths.  According to the official documentation , the paths are created with strings that look a lot like LOGO (if you’re old enough to remember turtles), say “M10,20L30,40” for “move to (10,20) then draw a line from there to (30,40)”.

To link two elements, in code, it basically looks like:

    // create the path string
    pathStr = 'M' + el1.x + ',' + el1.y + 'L' + el2.x + ',' + el2.y;
    // use it to create the path
    paper.path(pathStr);

What happens after you’ve created your string is that the library parses the string, and creates a nice little structure of arrays, each containing a command (the ‘L’ or ‘M’ or whatever) and the parameters for it.  Using that, the actual path is created.

That step, of breaking down the string I just constructed, was eating about 20% of my CPU when, originally, I already had all the elements on hand nice and distinct.

Optimization #2 using another undocumented feature: the official API is

    Paper.path([pathString])

but, if you have the structure on hand or can build it, the path() method will actually take that structure and use it directly.  Rather than creating the string, I now do this:

    // create the path *structure*
    pathArg = [['M', el1.x, el1.y], ['L', el2.x, el2.y]];
    // use it to create the path
    paper.path(pathArg);

and that’s it!  The method didn’t disappear from my radar, but did lose a lot of weight in the profiling.

Conclusion

It would be nice if these functions could be standardized, in order to future-proof my code a bit more and the bottleneck in my application is still the graphic generation, but now it is pretty usable and still has all the portability of HTML/JS.

If you have hints of your own about optimizing SVG generation, I’d love to hear about them.

Cheers!

Leave a Reply