dThis is a short story of suffering, pain, success and failures in an attempt to speed up RaphaelJS on big and complex SVGs.
In ResumUP we create a lot of colorful and meaningful resumes and vacancies
It all started with using RaphaelJS since it was the only library that enabled a fast implementation of designers’ hard work. When the first version of SVG resume was ready we faced, of course, a performance issue. The process of drawing resumes on the client side took upwards of 5-10 seconds in modern browsers — not to mention old browsers and computers, where it could take up to 30 seconds. Obviously, this was inadequate for both our needs and those of our customers.
Part 1. Tango and Cache.
So, what do we have?
- A lot of js code that draws SVGs
- Execution time of up to 30 seconds (5-7 seconds on average)
- Little time but a great deal of desire to tune-up this stuff a bit
Firstly, we needed to determine where the bottleneck of Raphael was occurring and soon realized that it was closely connected to DOM. Thus, every time we call some Raphael method it creates a new DOM node. This may seem OK, but working with DOM objects directly requires a lot of overhead, so it is always faster to just insert SVG as plain XML in what is called inline SVG. Unfortunately, this feature is only supported by modern browsers and I suspect the library’s author used the DOM way for the sake of compatibility. Imagine that you have a few thousand nodes in one SVG while at the same time the complicated logic of drawing resume elements. Clearly, it’s not a cool solution.
When we came to an understanding of the issue and realized that we couldn’t fix Raphael quickly, we decided to use the server side cache instead of speeding up js code.
That’s what we came up with:
- Users visit the resume.
- Each of 10 resume blocks is rendered on the client side and then sent to the server.
- The next time someone visits the resume, we just load data from the db and insert it as an inline SVG.
- If some blocks are missing, we fallback to Raphael, rendering them again
Well, it worked and reduced the required drawing time to only one second, which was amazingly fast for us after the previous requirement of 5-7 seconds. The only problem left was what to do about old browsers which do not support inline SVG. They still have to render a resume every time.
Part 2. SVG on server side and tears.
Again, we have:
- A lot of js responsible for drawing.
- Execution time of 1 to 30 seconds (funny, yeah).
- Some time and the necessity of drawing SVG on the server side.
As perfectionists by nature, we weren’t satisfied by these results and started looking for a more elegant solution. In addition, some ideas we came up with required an API for drawing images. We needed server side functionality. The one thing we knew for certain was that we didn’t want to rewrite the thousands (and I mean it) of lines of code responsible for rendering resumes in some other language (we have ruby on the back end).
In the beginning, we tried to solve this problem by straight running existing js code with rubyracer, a wrapper around V8, which can interpret js code inside ruby. But as we said, Raphael is tightly connected with DOM and there is no DOM in V8. Fail. After googling around a bit we found jsdom, a library that imitates a browser inside a V8 environment. Cut to a few hours later and we had managed to run everything we needed. But happiness didn’t last long, because the program failed on the tenth resume with a “could not allocate memory” message. After some investigation we found out that the process of rendering each resume took approximately 8 seconds and 50-70 MB were leaking with each iteration. Nevertheless, it was a success since we proved the concept.
We started fine tuning this scheme and, at first, replaced the rubyracer with NodeJS with no wrappers. We wrote simple HTTP client with ExpressJS and loaded all our code alongside it with jsdom here. Using this method, we got four seconds for each resume with 50-60 MB of leakage. Still, it wasn’t good enough, so to improve performance we obviously needed to get rid of jsdom, which was leaking. This, however, required a whole new approach.
Part 3. Baileys and Absinthe. First hope
After all these experiments we decided that we should not stop halfway. The new idea was simple: we wanted to split the process of drawing in two separate parts:
- Building an abstract model, which described a future picture.
- Mapping this model to SVG.
This approach helped us to resolve some issues:
- No more jsdom on the server.
- Take advantage of inline SVG in modern browsers with a fallback to working with DOM for client.
- Since we had an abstract model we could map it to different formats, for example VML for IE8 support.
To achieve this goal, we started with the idea of modifying Raphael. We wanted to divide it into two parts and extract the one which didn’t depend on DOM. However, a few hours spent in RaphaelJS 2.0 source code made us realize that it wouldn’t work or would take too much time. And the deadline was approaching.
Thinking quickly, we decided to write a copycat of Raphael with all the same methods we used in our legacy code, so it looked the same, but worked in the way we needed. Thus, we could use Raphael on the client side and this copycat on the server. We called it “Baileys”.
The User sends request to rails, rails call nodejs, nodejs processes data, renders it to json representing svg and sends it to rails, rails renders json to svg. We called the ruby part “Absinthe”.
This scheme appeared to be a success: rendering for 0.5 seconds with no leaks, but many questions.
Part 3. The Prototype.
We had a working solution, but it was far from production quality as we had some requirements:
- One code everywhere. We had to create one more entity to use it on server and on client sides.
- Less ruby is better. We wanted to port absinthe to js.
The final scheme looks like this:
- Tetris and — all code responsible for drawing svg without any dependencies on the library that creates svg.
- Baileys — a copycat of Raphael that creates an abstract model instead of DOM elements. In addition, we optimized such things as transformations and getBBox function.
- Absinthe — ported to js, works in tandem with Baileys and maps it to SVG.
- Polyomino — lightweight expressjs server that serves requests.
We are now able to use Tetris as an npm packet on the server and as asset pipeline gem on the client.
The current situation is 70 ms for each resume on average, which is super fast comparing with initial results.
Our next step is to port our current code to using baileys+absinthe scheme and stop using raphael. For those browsers, which do not support inline SVG we will insert it as img tag with src pointing to an actual svg image on s3. In case nodejs server is down, we can fallback to client side drawing as well.