Reducing string concatenations from ngTemplates

A new Chrome update suddenly broke our application. It improved string parsing, but at the same time makes extensive use of memory for a lot of string concatenations in a single expression. Such concatenation was used eavily by grunt-angular-templates used at work.

Identifying the problem

A few customers and coworkers at work started to notice crashes when using our application, or just an extensive of memory in the developer tools. Surprisingly, only the latest Chrome version was impacted. People not having upgraded or using Safari or Firefox did not notice the problem.

I personally use Firefox at work, so it came unoticed on my side.

A few coworkers tracked down the problem by removing parts of the javascript on our application and iterating to find the root cause of the problem. The ended up with a web page with only the templates.js generated by grunt-angular-templates.

The template looks like, as a basic example of how it is generated (with dummy example):

angular.module('moduleName').run(['$templateCache', function($templateCache) {
  'use strict';

  $templateCache.put('/templates/page.html',
    "<div>\n" +
    "{{$pageContent}}\n" +
    "</div>"
  );
  // lots of other templates
}]);

The string parsing in Chrome has recently been updated… and a bug was opened exactly matching the problem. That is why the relevant commit can be referred to.

Fixing string concatenations

A few options were available to fix the problem:

  • fork grunt-angular-templates and remove or reduce string concatenations:
    • add a new repository on github, involving the IT department for that
    • updating the build system to use the new repository
    • fix the library by changing the dedicated function
  • overwrite the template generation file:
    • equivalent to forking with less infrastructure
    • patch the needed file directly by overriding the function that generates the string concatenation
    • add it to the build process just after package installation
    • this was the other option that I highly considered, but needing to add a build step, and maintain a new (patch) file, I preferred it being explicit by staying in the package boundary
  • use a few regular expressions to fix the generated template as a post-processing step:
    • I am wary of such tools to replace strings, and correctly manage escaping (single quotes, double quotes, backquotes, …)
    • it would add a post-processing step
    • the complexity and readability is equivalent to rewriting the file
  • modify the template and evaluate to rewrite the concatenated expressions as a single string:
    • add a new post-processing step, equivalent to using regular expressions
    • simpler and safer

I decided to go for the latter.

Rewriting the template file

grunt-angular-templates takes a boostrap argument to rewrite what will be generated. I replaced it in our Gruntfile.js by

ngtemplates: {
  moduleName: {
    options: {
      bootstrap: (module, script, options) => `const $templateCache = {};\nconst realCache = {};\n$templateCache.put = (k, v) => { realCache[k] = v};\n${script}\nmodule.exports = { realCache };\n`,
    },
    src: [
      ...
    ],
    dest: "templates.tmp.js",
  }
}

and renamed the generate file. All templates are now put in a realCache variable that is exported.

A new step was then added to the build process to:

  • read that file
  • evaluate each template from realCache as a string using JSON.stringify()
  • write it back with the original name

which can be summarized to:

const { realCache } = require(inputFile);

const content = [`angular.module('${moduleName}').run(['$templateCache', function($templateCache) {\n'use strict';\n`];
for (const [k, v] of Object.entries(realCache)) {
    content.push(`$templateCache.put("${k}", ${JSON.stringify(v)});`);
}
content.push(["}]);\n"]);

The output file now looks like:

angular.module('moduleName').run(['$templateCache', function($templateCache) {
'use strict';

$templateCache.put("/templates/page.html", "<div>\n{{$pageContent}}\n</div>");
// lots of other templates
}]);

String concatenations were effectively removed.

Conclusion

Using this method, the previous script (and a few line of comments to explain it) was added to the build step, and the rest was left as-is. It resulted in:

  • Memory down from 1.7GB to 120MB in our application using the latest Chrome version
  • A faster evaluation and load time due to string concatenations removal (nice side effect!)
  • No noticeable changes in Firefox and Safari

We needed to backport the fix to earlier versions, but it was relatively simple. We’re never safe from third party components updates…