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,678