Reducing string concatenations from ngTemplates

A new Chrome update suddenly broke our application at work. It improved string parsing, but at the same time significantly increased memory usage due to excessive string concatenations in a single expression. This pattern was heavily used by grunt-angular-templates in our codebase at work.

Identifying the problem

A few customers and coworkers started noticing crashes when using the application, or significant memory consumption in the browser’s developer tools.
Interestingly, only the latest version of Chrome was affected. Users who had not updated Chrome, or who were using browsers like Safari or Firefox, did not encounter the issue.

As I personally use Firefox at work, the problem initially went unnoticed on my side.

Some coworkers began investigating by gradually removing parts of the JavaScript loaded by the application. Through this process of elimination, they eventually isolated the issue to the templates.js file generated by grunt-angular-templates.

Here is a simplified example of what a generated template might look like:

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 was recently updated, and a bug report was opened that exactly matches the issue we encountered. The corresponding commit in the V8 engine can be referenced as the root cause.

Fixing string concatenations

Several options were available to fix the issue:

  • fork grunt-angular-templates and reduce string concatenations:

    • create a new repository on GitHub, involving the IT department as required
    • update the build system to use the new repository
    • fix the library by modifying the relevant function responsible for generating string concatenations
  • overwrite the template generation file:

    • equivalent to forking, but with less infrastructure
    • patch the required file directly by overriding the function that performs string concatenation
    • add the patched file to the build process immediately after package installation
    • I considered this approach, but it would have required maintaining a patch file and adding a build step. I preferred a more explicit solution that remained within the boundaries of the package
  • use regular expressions to fix the generated template as a post-processing step:

    • I am generally cautious with such solutions, as correctly handling escaping (single quotes, double quotes, backticks, etc.) can be error-prone.
    • this approach would require a new post-processing step
    • the complexity and readability would be similar to rewriting the file altogether
  • modify the template and evaluate rewriting the concatenated expressions as a single string:

    • introduce a custom post-processing step, but instead of using regular expressions, provide a clean replacement
    • this is simpler and safer than using pattern matching or string substitution

I chose the last option.

Rewriting the template file

grunt-angular-templates accepts a bootstrap argument that allows customization of the generated output. I used this option to replace the default behavior in our Gruntfile.js.

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 generated file. All templates are now placed 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

This can be summarized as:

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 lines of comments to explain it) was added to the build step, and the rest was left unchanged. It resulted in:

  • memory usage reduced from 1.7GB to 120MB in our application using the latest Chrome version
  • faster evaluation and load time due to the removal of string concatenations (a 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 are never completely safe from updates to third-party components…