Tuning SVG Performance in a Production Application

About Me

Rohit Kalkur

Software Engineer @ Social Tables

Twitter: @Rovolutionary

Github: rovolution

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?

  1. Create a Document Fragment.
  2. 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

Made with Slides.com