Tuning SVG Performance in a Production Application
About Me
About Social Tables
- Based in Washington D.C.
- SaaS-based company with focus on event planning/management
- Venue Mapper - "AutoCAD for events"
- All objects in Venue Mapper are SVG!
-
SVG allows us to:
- preserve quality
- make objects highly customizable
- Some objects consist of multiple SVGs
<svg>
<circle class="table selected" r="36" style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0);"></circle>
<g transform="translate(0, -36) rotate(180) rotate(0,0,-36)scale(1,1)">
<g style="overflow: visible" class="selected">
<path transform="scale(2)" d="M -8,0 v 8 c 0,4 4,8 8,8 c 4,0 8,-4 8,-8 v -8 z" class="ring" style="stroke: none; fill: rgba(0, 0, 0, 0);" ></path>
<path d="M -8,0 v 8 c 0,4 4,8 8,8 c 4,0 8,-4 8,-8 v -8 z" class="ring" style="stroke: rgb(107, 103, 103); fill: rgba(255, 255, 255, 0.2);"></path>
</g>
</g>
<g transform="translate(0, -36) rotate(180) rotate(30,0,-36)scale(1,1)">
<g style="overflow: visible" class="selected">
<path transform="scale(2)" d="M -8,0 v 8 c 0,4 4,8 8,8 c 4,0 8,-4 8,-8 v -8 z" class="ring" style="stroke: none; fill: rgba(0, 0, 0, 0);" ></path>
<path d="M -8,0 v 8 c 0,4 4,8 8,8 c 4,0 8,-4 8,-8 v -8 z" class="ring" style="stroke: rgb(107, 103, 103); fill: rgba(255, 255, 255, 0.2);"></path>
</g>
</g>
</svg>
- Everyday we have hundreds of customers creating events
- It is critical to create the best user experience possible!
- Templates allow a user to click and drag to create a continous region of tables
- Very useful when trying to lay out tables for large venues such as a ballroom.
Templates
Let's try this with 150 tables!
- This slow performance is unnacceptable!!
- How can I improve this feature so that is performs seamlessly for hundreds of tables?
Why not try <use>?
- Since the SVG's for a chair and table are complex, create symbol's for each and create instances of them with <use>
- Maybe this will speed up the render time
Experiment Set-Up
- Create a simple web app that takes row/column numbers and generates row * column number of tables
- First generate tables with <use>, then without <use>
- Compare the rendering times and see if there is a performance boost
How to create a table with <use>?
<!-- Chair SVG Definition-->
<symbol id="chair">
<g style="overflow: visible" class="selected">
<path transform="scale(2)" d="M -8,0 v 8 c 0,4 4,8 8,8 c 4,0 8,-4 8,-8 v -8 z" class="ring" style="stroke: none; fill: rgba(0, 0, 0, 0);" ></path>
<path d="M -8,0 v 8 c 0,4 4,8 8,8 c 4,0 8,-4 8,-8 v -8 z" class="ring" style="stroke: rgb(107, 103, 103); fill: rgba(255, 255, 255, 0.2);"></path>
</g>
</symbol>
<!-- Table SVG Definition-->
<symbol id="table">
<circle class="base-table selected" r="36" style="stroke: rgb(0, 0, 0); fill: rgb(255, 255, 255)"></circle>
<use xlink:href="#chair" transform="translate(0, -36) rotate(180) rotate(0,0,-36) scale(1,1)"></use>
<use xlink:href="#chair" transform="translate(0, -36) rotate(180) rotate(30,0,-36) scale(1,1)"></use>
<use xlink:href="#chair" transform="translate(0, -36) rotate(180) rotate(60,0,-36) scale(1,1)"></use>
<use xlink:href="#chair" transform="translate(0, -36) rotate(180) rotate(90,0,-36) scale(1,1)"></use>
</symbol>
<!-- Using the symbol to create 3 tables -->
<use xlink:href="#table" transform="translate(0, -36)"></use>
<use xlink:href="#table" transform="translate(0, -56)"></use>
<use xlink:href="#table" transform="translate(0, -76)"></use>
With <use> (5000 tables)
Without <use> (5000 tables)
- <use> is actually worse for performance!
-
My hunch?
- MDN definition: The use element takes nodes from within the SVG document, and duplicates them in a non-exposed DOM
- The SVG nodes that make up a <use> tag still exist on the page
- No real reduction in DOM elements
- Lets try another approach!
- But first, lets get a better understanding of the Venue Mapper core technology stack
Background: Knockout.js
- Venue Mapper is build with Knockout.js
- Knockout JS has a data structure called observable arrays, which can be referenced in HTML
- When an JS object is added to an observable array referenced with the keyword foreach, it will render a copy of the markup bound to that object.
Background: Knockout.js
var shapes = ko.observableArray();
var circle1 = {
x: 30,
y: 30,
color: "red"
};
var circle2 = {
x: 90,
y: 90,
color: "green"
};
shapes.push(circle1);
shapes.push(circle2);
Background: Knockout.js
<svg>
<!-- ko foreach: shapes() -->
<circle r="40" stroke="black" data-bind="attr: {cx: x, cy: y, fill: color}" />
<!-- /ko -->
</svg>
Background: Knockout.js
Background: Reflow
- Definition: Web browser process for re-calculating the positions/geometries of elements in the document.
- Reflowing an element can sometimes require reflowing its parent elements and any elements which follow it
- Very expensive!!!
Why are previews so slow?
- Each table preview is created as a JavaScript object and then appended to a Knockout observable array of "floorObjects"
- Once added to "floorObjects", the SVG representation of the table is appended to the DOM
- For each table preview appended to the DOM, a reflow cycle occurs...This is bad!!!
Background: Document Fragments
- Definition: Lightweight container for holding DOM nodes
- Used to append a large number of nodes to the DOM at once
/*
Assuming that 'shapes' refers to the array of shape data in earlier slide
*/
var fragment = document.createDocumentFragment();
for(var i = 0; i < shapes.length; i++) {
// Create 'circle' element
var circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
// Set attributes on circle
circle.setAttribute("cx", shapes[i].x);
circle.setAttribute("cy", shapes[i].y);
circle.setAttribute("fill", shapes[i].color);
// Append circle to fragment
fragment.appendChild(circle);
}
// Append the single fragment to the SVG element
document.getElementByTagName("svg").appendChild(fragment);
Background: Document Fragments
How can we speed this up?
- Generate the markup for a single table preview by appending it to the observable array.
- Keep a reference to this preview DOM element by assigning it an id: "#template-preview"
- Style the element with this id to have "display: none"
var table = {
numChairs: 12,
color: "white",
radius: 10
}
- Why do we need the markup for just a single table?
- Certain properties of the table SVG (such as chairs, color, and radius) are dependant on the value of the corresponding JavaScript object
- We need to append one of these objects to the floorObjects observable array in order to generate the correct table SVG
How can we speed this up?
- Create a Document Fragment.
- For each preview element to be created:
- use cloneNode() on the #template-preview element to create a copy of the node
- Set the x,y positional properties on this new node
- Append new node to the document fragment
3. Append this document fragment to the SVG element
Did this work?
Can we do better?
Object Pool
- Create two JS arrays: one for all "used" previews (usedPreviewNodes) and one for all "not used"(notUsedPreviewNodes)
- As previews nodes are created, add them to the usedPreviewNodes
- As the user contracts the region, mark any extra nodes as "not used" and add them to notUsedPreviewNodes
Object Pool
- "Not used" nodes are tagged with a class that sets display: none
- If the user expands the region, convert the usedPreviewNodes, and then recycle any notUsedPreviewNodes
- When the user is done previewing the template, set these arrays back to empty arrays (to prevent memory leaks)
Object Pool
- Instead of removing the preview nodes (which triggers reflow), we hide them
-
Why is this faster?
- Less reflow == better performance
1. Expand to 53 tables
- 53 tables in usedPreviewNodes
- 0 tables in notUsedPreviewNodes
2. Contract to 22 tables
- 22 tables in usedPreviewNodes
-
31 tables in notUsedPreviewNodes
- These nodes are hidden, but not removed!
3. Expand to 76 tables
- 22 nodes repositioned
- 31 notUsedPreviewNodes recycled
- 23 new nodes created
Performance Impovement?
Start to End
Original
Refactored
Performance Data
Original
Refactored
Pros
- Template previews are much faster!!
- Smoother user experience
Cons
-
Have to wait to generate actual tables
- "Working..." UI block
- My opinion: This impact of this tradeoff is minimal
- More important to ensure that performance is good for very interaction-heavy features
The End
Tuning SVG Performance In a Production App
By Rohit Kalkur
Tuning SVG Performance In a Production App
- 14,145