0

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.

Michal Charemza
  • 25,940
  • 14
  • 98
  • 165

2 Answers2

1

Try this:

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.push((i == 0 ? 'M' : 'L') + coords[i][0] + ',' + coords[i][1]);
}
d.push('z');

pathElement.setAttributeNS(null, 'd', d.join(''));
kshetline
  • 12,547
  • 4
  • 37
  • 73
  • https://stackoverflow.com/questions/16696632/most-efficient-way-to-concatenate-strings-in-javascript and https://jsperf.com/join-concat/2 seem to suggest that at least in some cases, using a join over += will be slower in most current browsers. – Michal Charemza Apr 28 '18 at 07:59
  • I just tried my own test (https://jsperf.com/string-concat-vs-join-random-numbers/) and the performance was about equal on Chrome, but noticeably better for concatenation with Firefox. Very surprising! Maybe all of those intermediate strings aren't actually being created, but are somehow cleverly being optimized away. – kshetline Apr 28 '18 at 08:46
  • After reworking my test cases a bit, concatenation is also significantly faster in Chrome too. – kshetline Apr 28 '18 at 09:24
0

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...

  • Manually find the ASCII characters from a number
  • Set the characters in an Uint8Array
  • Use new TextDecoder.decode(.... to get a Javascript string from the buffer

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);
Michal Charemza
  • 25,940
  • 14
  • 98
  • 165