javascriptsvggarbage-collectionmap-projectionsmercator

Create complex SVG without intermediate strings


I'm creating/editing a lot (100s to 1000s) of SVG path elements, with integer coordinates, in real time in response to user input (dragging).

var pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
var coords = [[0,0], [1,0], [1,1], [0,1]]; // In real case can be list of 1000s, dynamically generated

var d = '';
for (var i = 0; i < coords.length; ++i) {
  d += (i == 0 ? 'M' : 'L') + coords[i][0] + ',' + coords[i][1];
}
d += 'z';

pathElement.setAttributeNS(null, 'd', d);

I can and do pool the path elements, so it minimises creation of objects + garbage in that respect. However, it seems to be that a lot of intermediate strings are created with the repeated use of +=. Also, it seems a bit strange to have the coordinates as numbers, convert them to strings, and then the system has to parse them back into numbers internally.

This seems a bit wasteful, and I fear jank since the above is repeated during dragging for every mousemouse. Can any of the above be avoided?


Context: this is part of a http://projections.charemza.name/ , source at https://github.com/michalc/projections, that can rotate the world map before applying the Mercator projection to it.


Solution

  • There is a method, using Uint8Array and TextDecoder that seems faster than string concatenation in Firefox, but slower than string concatenation in Chrome: https://jsperf.com/integer-coordinates-to-svg-path/1.

    Intermediate strings are not created, but it does create a Uint8Array (a view on an a re-useable ArrayBuffer)

    You can...

    As below

    // Each coord pair is 6 * 2 chars (inc minuses), commas, M or L, and z for path
    var maxCoords = 1024 * 5;
    var maxChars = maxCoords * (2 + 6 + 1 + 1) + 1
    var coordsString = new Uint8Array(maxChars);
    var ASCII_ZERO = 48;
    var ASCII_MINUS = 45;
    var ASCII_M = 77;
    var ASCII_L = 76;
    var ASCII_z = 122;
    var ASCII_comma = 44;
    var decoder = new TextDecoder();
    var digitsReversed = new Uint8Array(6);
    
    function concatInteger(integer, string, stringOffset) {
      var newInteger;
      var asciiValue;
      var digitValue;
      var offset = 0;
    
      if (integer < 0) {
        string[stringOffset] = ASCII_MINUS;
        ++stringOffset;
      }
      integer = Math.abs(integer);
    
      while (integer > 0 || offset == 0) {
        digitValue = integer % 10;
        asciiValue = ASCII_ZERO + digitValue;
        digitsReversed[offset] = asciiValue;
        ++offset;
        integer = (integer - digitValue) / 10;
      }
    
      for (var i = 0; i < offset; ++i) {
        string[stringOffset] = digitsReversed[offset - i - 1];
        ++stringOffset
      }
    
      return stringOffset;
    }
    
    
    var pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    
    var coordsStringOffset = 0;
    var coords = [[0,0], [1,0], [1,1], [0,1]]; // In real case can be list of 1000s, dynamically generated
    for (var i = 0; i < coords.length; ++i) {
      coordsString[coordsStringOffset] = (i == 0) ? ASCII_M : ASCII_L;
      ++coordsStringOffset;
      coordsStringOffset = concatInteger(coords[i][0], coordsString, coordsStringOffset);
      coordsString[coordsStringOffset] = ASCII_comma
      ++coordsStringOffset;
      coordsStringOffset = concatInteger(coords[i][1], coordsString, coordsStringOffset);
    }
    coordsString[coordsStringOffset] = ASCII_z;
    ++coordsStringOffset;
    
    var d = decoder.decode(new Uint8Array(coordsString.buffer, 0, coordsStringOffset));
    
    pathElement.setAttributeNS(null, 'd', d);