Search code examples
node.jstypescriptcommand-line-interfacees6-modules

ES6/TypeScript Dynamic Imports - Slow launch time because of static imports (Nodejs CLI)


In my nodejs-code, I have replaced many require(x) with ES6 import { ... } from "x". Unfortunately, this is very bad for the launch time of my nodejs-CLI-program.

Let me first give you a little bit of background:

I have a nodejs-CLI-program that uses a whole bunch of external packages, but most of those packages are only required under rare circumstances (for example, if specific flags were passed to my program). Those imports impose a significant overhead for the launch time of my program. Even trivial commands like myprogram --help take more than one second because of those imports.

To fix this launch time problem, I would like to do "dynamic imports" for certain functionality. In other words, I would like to import certain packages only if a specific functionality is actually needed by a given CLI-command.

With the old require-mechanics, this was trivially possible with a conditional call to require. However, I am not sure how to do this for modern TypeScript-code.

Please let me know if you have any suggestion for this kind of launch time problems.


Solution

  • You can use Dynamic Import Expressions in TypeScript:

    Dynamic import expressions are a new feature and part of ECMAScript that allows users to asynchronously request a module at any arbitrary point in your program.

    This means that you can conditionally and lazily import other modules and libraries. For example, here’s an async function that only imports a utility library when it’s needed.

    (In JavaScript, it's still a proposal: https://github.com/tc39/proposal-dynamic-import)

    Example:

    You have a main file and two dependencies.

    ./main.ts
    ./dependency-a.ts
    ./dependency-b.ts
    

    Dependency 'a' will load fast.

    console.log('exporting dependency-a');
    
    export const a = () => {
      console.log('called dependency-a');
    };
    

    While dependency 'b' will load slowly.

    console.log('exporting dependency-b');
    
    // We'll emulate a slow synchronous task with a loop to add delay
    // https://stackoverflow.com/a/38839049/4669212
    function wait(ms: number) {
      var start = Date.now(),
        now = start;
      while (now - start < ms) {
        now = Date.now();
      }
    }
    
    wait(5000);
    
    export const b = () => {
      console.log('called dependency-b');
    };
    

    In your main file, you call the exported functions conditionally, but the launch time will be slow because of dependency 'b', even if you just want to call dependency 'a':

    import { a } from './dependency-a';
    import { b } from './dependency-b';
    
    const run = (dep: 'a' | 'b') => {
      switch (dep) {
        case 'a':
          return a();
    
        case 'b':
          return b();
    
        default:
          console.log('do nothing');
      }
    };
    
    run();
    

    What you can do is to use the dynamic import() expression like this:

    const run = (dep: 'a' | 'b') => {
      switch (dep) {
        case 'a':
          return import('./dependency-a').then(({ a }) => {
            a();
          });
        case 'b':
          return import('./dependency-b').then(({ b }) => {
            b();
          });
        default:
          console.log('do nothing');
      }
    };
    
    run('a');
    

    The slow dependency 'b' - and it's import statements, if there's any - will not be loaded when you run dependency 'a'. That means your CLI will have a better startup time.