Let me start with a story. Once upon a time I stumbled upon an excellent article by Philip Walton where he describes how expensive script evaluations could (and should!) be deferred until the browser is idle or they are actually needed. One of the examples that awakened my interest was creating an instance of the Intl.DateTimeFormat object, as I was using this great API quite often but never thought it can cause real performance problems. Turns out it can, especially if used inside loops. Apart from the technique described in Philip’s article, another solution is to simply reuse Intl.DateTimeFormat instances instead of creating them every time.

And that’s what I always did out of force of habit — if I need something again, why not keep a reference to it instead
of adding garbage collector more work? But my curiosity told me to search through our git repository and as a result I discovered multiple usages of Intl.DateTimeFormat without caching, some of which were inside loops, possibly leading to performance problems. After I informed responsible teams and shared my discovery on our company-wide frontend chat, one of my colleagues suggested that we should prevent similar issues in the future by writing an ESLint rule. I have never done that so I eagerly took this opportunity to learn something new.

For those who don’t know, ESLint is a very popular Javascript linter (a tool used to enforce certain code style), with various applications: mainly error prevention and consistent formatting. Although sometimes it can be annoying (imagine doing a quick fix and finding out that the line you have changed exceeds maximum length), it adds great value to every Javascript application. One of its key features is the possibility of writing additional rules, which thanks to the open source culture led to many useful projects like eslint-plugin-react or typescript-eslint.

At Allegro we have our own sets of rules, one of which is enabled by default in every Opbox component. It contains mostly performance-oriented suggestions like:

My task was to extend this set of rules to prevent calling format(...) method on Intl.DateTimeFormat instance immediately after creating it:

// good
const formatter = new Intl.DateTimeFormat('en-US');
formatter.format(new Date());

// bad
new Intl.DateTimeFormat('en-US').format(new Date());

In short, ESlint works by traversing an AST (abstract syntax tree) representation of the code and applying given rules whenever it matches their pattern (and by the way this is also how Babel works). In order to create a rule we need to take a look at bad code’s AST and try to make a pattern out of it:

{
  "type": "Program",
  "start": 0,
  "end": 52,
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 0,
      "end": 52,
      "expression": {
        "type": "CallExpression",
        "start": 0,
        "end": 51,
        "callee": {
          "type": "MemberExpression",
          "start": 0,
          "end": 39,
          "object": {
            "type": "NewExpression",
            "start": 0,
            "end": 32,
            "callee": {
              "type": "MemberExpression",
              "start": 4,
              "end": 23,
              "object": {
                "type": "Identifier",
                "start": 4,
                "end": 8,
                "name": "Intl"
              },
              "property": {
                "type": "Identifier",
                "start": 9,
                "end": 23,
                "name": "DateTimeFormat"
              },
              "computed": false
            },
            "arguments": [
              {
                "type": "Literal",
                "start": 24,
                "end": 31,
                "value": "en-US",
                "raw": "'en-US'"
              }
            ]
          },
          "property": {
            "type": "Identifier",
            "start": 33,
            "end": 39,
            "name": "format"
          },
          "computed": false
        },
        "arguments": [
          {
            "type": "NewExpression",
            "start": 40,
            "end": 50,
            "callee": {
              "type": "Identifier",
              "start": 44,
              "end": 48,
              "name": "Date"
            },
            "arguments": []
          }
        ]
      }
    }
  ],
  "sourceType": "module"
}

After looking at more examples of usage, we can deduct that we are looking for a node:

  • of type MemberExpression (because we want to prevent directly accessing members of a new instance),
  • with object.type equal NewExpression and object.callee.object.name equal Intl (because we want to apply our rule only to new instances of Intl members)

We could be more specific about the second point but we want to also cover other Intl utilities like Intl.NumberFormat, Intl.Collator and so on.

Using pathEq from nanoutils for null-safety the complete code looks like this:

const { pathEq } = require('nanoutils');

// does node.object.callee.object.name equal 'Intl'?
function isIntlApi(node) {
  return pathEq(['object', 'callee', 'object', 'name'], 'Intl', node);
}

// does node.object.type equal 'NewExpression'?
function isInstanceCreation(node) {
  return pathEq(['object', 'type'], 'NewExpression', node);
}

module.exports = {
  create(context) {
    return {
      // we are looking only for MemberExpressions
      MemberExpression: (node) => {
        if (isIntlApi(node) && isInstanceCreation(node)) {
          context.report(node, 'Intl object instances are expensive, consider reusing them.');
        }
      },
    };
  },
};

Finally, we should add some tests to verify that we didn’t miss any cases. Then we can include the rule in our eslint config and we are ready to go! I hope this post will encourage you to play a little bit with ESLint/Babel and AST.