Tuning RaphaelJS for High Performance SVG Interfaces

Jul 25 2012, 12:30 PM PDT · 2 comments »

Last week, we released a few improvements to our front-end that significantly improved the performance of working with larger schematics in CircuitLab. In this blog post, we detail one of the key components to this improvement. Since it makes modifications to RaphaelJS, a very handy vector graphics library, we thought releasing the details might be helpful to other developers of high-performance browser-based applications.

One of the key interactions we wanted to improve was the performance of zooming and panning around large circuits. The CircuitLab front-end makes use of the Raphael library to draw and manipulate SVGs. In our earlier implementation of zooming, we were using the scale() method on a Raphael Set object that contained all the elements in the circuit. It turns out that the complexity of doing any operation on a Raphael Set is linear with the number of elements in the set. To convince yourself of this, you can read through the Raphael Set source code, or you can see our first experiment below. For each of these tests, I placed N current sources into the circuit, and then recorded how long it took to zoom the schematic from 100% to 200% and back to 100% ten times.

Old Implementation

While the roughly ~0.3 seconds needed to zoom a small number of elments was acceptable, as soon as circuits grew to 20 or 30 elements, zooming became quite slow. This meant that our front-end wasn't great for the layout and sharing of complex schematics.

The issue here is that a Raphael Set is essentially implemented as an array that applies any operations on it to each of its items. It's a nice little way of grouping elements together, but unfortunately there is a performance penalty that in this case can be avoided. Luckily, the SVG specification gives us a way to solve this problem in a rather elegant way. SVG provides a group element <g>, inside which you can nest other SVG elements. You can then apply transformations to the group element, and the browser then takes care of propagating the transformation down to the group's inner elements -- even using the graphics card if your system supports it.

To take advantage of this, all we needed to do was find a way to wrap all the SVG elements that Raphael creates into one single big group. This lets us apply SVG transformations (which represent zoom and pan operations) to the group, instead of applying them to a Raphael Set. Implementing this turned out to be a fairly simple modification of the Raphael source code.

Basically, we intercepted the root SVG element as it's being created and assigned as the canvas (the parent of all other elements) within Raphael's internals. Instead, we created an SVG root element <svg> with a group element <g> inside it, and keep a reference to the group. This group element now becomes the canvas for all other Raphael operations. Once we do this, all the SVG elements that Raphael creates will be children of this <g> element.

4747    //these are custom additons from circuitlab to make a single
4748    //group around all the elements
4749    var g = $("g")
4750    cnvs.appendChild(g)
4751    cnvs = g
4752    g.style && (g.style.webkitTapHighlightColor = "rgba(0,0,0,0)");
4753    //end group modifications

We also added a function called setTransform to the Raphael paper prototype:

//special functions added for circuitlab, assumes canvas is actually
//a group, not the svg element
R.prototype.setTransform = function (trans) {
    $(this.canvas,{transform:trans});
}

This allows us to set the transform string of the <g> element directly on the paper element. For example:

var paper = Raphael(10, 50, 320, 200);
paper.setTransform('translate(10,10)');

We did have to make the following change in the viewPort method so Raphael could correctly resolve the <svg> element now that we changed what cnvs points to. This change was done on line 4420 of the original Raphael code:

pos = cnvs.getScreenCTM() || cnvs.parentNode.createSVGMatrix(),

Using this method to zoom and pan around our circuits (unsurprisingly) gives us a very significant speedup over utilizing Raphael Sets. Below are the results of the same experiment we ran before:

New Implementation

Note the y-axis values! Here are the results for both old and new experiments with a logarithmic axis for comparison:

Both Implementations

These simple changes to the Raphael library allowed us to see massive improvements across our benchmark, with a >100X performance gain at the higher end, and better asymptotic performance which makes larger circuits workable. Give our browser-based schematic capture and circuit simulation tool a spin with one of our example circuits!

One caveat here is that the changes we applied only operate within the SVG module of Raphael. Since CircuitLab doesn't currently support Internet Explorer, this isn't a concern for us, however if you rely on Raphael for IE support you will also have to implement the setTransform() method appropriately in the VML module. Here is a link to the change set that shows the changes discussed in this post.


Comments

https://www.circuitlab.com/blog/2012/07/25/tuning-raphaeljs-for-high-performance-svg-interfaces/

I've tried the above link in both Chrome 21.0.1180.89 and IE 9.0.8112.16421. The formatting looks all wrong. For instance, 1) the URLs of the links/images are shown in plain text, instead of "hover over".

2) The images are not displayed inline. 3) The post is incomplete -- it ends with this text: " within Raphael's internals. Instead, we created an SVG root element ".

Google found a 9/11/2012 version of this exact same page/url that looks properly formatted. The post looks complete. The above text continues with " <svg> with a group element <g> inside it ....." and you can see all the images (graphs) inline.

That kinda makes me wonder whether it was the "<svg>" that is causing the problem.

Hope that helps you clean things up.

--Erik

by eostermueller
September 16, 2012

Hi there,

This sounds really interesting, but I'm having problems with it. Replacing Raphael 2.0 with this variant causes functionality based on .setViewBox() (the standard method for panning and zooming in Raphael, as far as I can tell) to break. Panning and re-sizing/zooming no longer happen at all, and also, the Raphael paths are no longer clipped by the paper boundaries.

What modifications does an app using .setViewBox() have to make when switching from regular Raphael to this variant? What's the intended approach to panning and zooming? I believe that using transforms to pan and zoom is not advised in Raphael as it results in unnecessary redrawing (I might be wrong there).

(also, the comma instead of a semicolon on the end of the line you changed - line 4803 - gives me a syntax error in Firefox, I had to change it back to a semi-colon before anything would work at all)

by alanomaly
October 21, 2012

Leave a Comment

Please sign in or create an account to comment.

Home · My Workbench · Plans & Pricing · Blog · Forums · About Us · Contact Us · FAQ · Terms of Service · Privacy Policy · Twitter ·