Skip to content

ESM import of http is slower compared to CommonJS #59686

Description

@bpasero

Version

v22.18.0

Platform

Darwin MBP-14-Work.local 24.6.0 Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:40 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6041 arm64

Subsystem

No response

What steps will reproduce the bug?

Run the following performance test from the command line:

#!/usr/bin/env node

const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');

// Create test files
const withHttpCode = `
import { STATUS_CODES } from 'http';
console.log(Object.keys(STATUS_CODES).length);
const startTime = process.hrtime.bigint();
const endTime = process.hrtime.bigint();
console.log(Number(endTime - startTime) / 1000000); // Convert to milliseconds
`;

const withoutHttpCode = `
const { STATUS_CODES } = require('http');
console.log(Object.keys(STATUS_CODES).length);
const startTime = process.hrtime.bigint();
const endTime = process.hrtime.bigint();
console.log(Number(endTime - startTime) / 1000000); // Convert to milliseconds
`;

// Write test files
fs.writeFileSync('with-http.js', withHttpCode);
fs.writeFileSync('without-http.js', withoutHttpCode);

async function runBenchmark() {
  const iterations = 100;
  const withHttpTimes = [];
  const withoutHttpTimes = [];

  console.log(`Running benchmark with ${iterations} iterations...\n`);

  // Benchmark with http import
  console.log('Testing with http import...');
  for (let i = 0; i < iterations; i++) {
    const startTime = process.hrtime.bigint();

    const child = spawn('node', ['with-http.js'], { stdio: 'pipe' });

    await new Promise((resolve) => {
      let output = '';
      child.stdout.on('data', (data) => {
        output += data.toString();
      });

      child.on('close', () => {
        const endTime = process.hrtime.bigint();
        const totalTime = Number(endTime - startTime) / 1000000; // Convert to milliseconds
        withHttpTimes.push(totalTime);
        resolve();
      });
    });

    if (i % 10 === 0) process.stdout.write('.');
  }
  console.log(' Done!');

  // Benchmark without http import
  console.log('Testing without http import...');
  for (let i = 0; i < iterations; i++) {
    const startTime = process.hrtime.bigint();

    const child = spawn('node', ['without-http.js'], { stdio: 'pipe' });

    await new Promise((resolve) => {
      let output = '';
      child.stdout.on('data', (data) => {
        output += data.toString();
      });

      child.on('close', () => {
        const endTime = process.hrtime.bigint();
        const totalTime = Number(endTime - startTime) / 1000000; // Convert to milliseconds
        withoutHttpTimes.push(totalTime);
        resolve();
      });
    });

    if (i % 10 === 0) process.stdout.write('.');
  }
  console.log(' Done!\n');

  // Calculate statistics
  function calculateStats(times) {
    const sorted = times.sort((a, b) => a - b);
    const sum = times.reduce((a, b) => a + b, 0);
    const avg = sum / times.length;
    const median = sorted[Math.floor(sorted.length / 2)];
    const min = Math.min(...times);
    const max = Math.max(...times);

    return { avg, median, min, max };
  }

  const withHttpStats = calculateStats(withHttpTimes);
  const withoutHttpStats = calculateStats(withoutHttpTimes);

  // Display results
  console.log('='.repeat(60));
  console.log('BENCHMARK RESULTS');
  console.log('='.repeat(60));

  console.log('\nWith HTTP import:');
  console.log(`  Average:  ${withHttpStats.avg.toFixed(2)} ms`);
  console.log(`  Median:   ${withHttpStats.median.toFixed(2)} ms`);
  console.log(`  Min:      ${withHttpStats.min.toFixed(2)} ms`);
  console.log(`  Max:      ${withHttpStats.max.toFixed(2)} ms`);

  console.log('\nWithout HTTP import:');
  console.log(`  Average:  ${withoutHttpStats.avg.toFixed(2)} ms`);
  console.log(`  Median:   ${withoutHttpStats.median.toFixed(2)} ms`);
  console.log(`  Min:      ${withoutHttpStats.min.toFixed(2)} ms`);
  console.log(`  Max:      ${withoutHttpStats.max.toFixed(2)} ms`);

  const avgDiff = withHttpStats.avg - withoutHttpStats.avg;
  const medianDiff = withHttpStats.median - withoutHttpStats.median;

  console.log('\nDifference (with HTTP - without HTTP):');
  console.log(`  Average:  ${avgDiff.toFixed(2)} ms (${((avgDiff / withoutHttpStats.avg) * 100).toFixed(1)}%)`);
  console.log(`  Median:   ${medianDiff.toFixed(2)} ms (${((medianDiff / withoutHttpStats.median) * 100).toFixed(1)}%)`);

  console.log('\n' + '='.repeat(60));

  // Cleanup
  fs.unlinkSync('with-http.js');
  fs.unlinkSync('without-http.js');
}

// Run the benchmark
runBenchmark().catch(console.error);

How often does it reproduce? Is there a required condition?

Always when I run the performance test.

What is the expected behavior? Why is that the expected behavior?

Importing a constant from http should not be significantly slower when using import vs require.

What do you see instead?

Using import is a lot slower:

Running benchmark with 100 iterations...

Testing with http import...
.......... Done!
Testing without http import...
.......... Done!

============================================================
BENCHMARK RESULTS
============================================================

With HTTP import:
  Average:  26.88 ms
  Median:   26.08 ms
  Min:      25.19 ms
  Max:      67.84 ms

Without HTTP import:
  Average:  17.54 ms
  Median:   17.44 ms
  Min:      16.66 ms
  Max:      21.56 ms

Difference (with HTTP - without HTTP):
  Average:  9.33 ms (53.2%)
  Median:   8.64 ms (49.5%)

============================================================

Additional information

Is it possible that the lazy undici method gets run when using import?

node/lib/http.js

Lines 118 to 124 in 5af0355

/**
* Lazy loads WebSocket, CloseEvent and MessageEvent classes from undici
* @returns {object} An object containing WebSocket, CloseEvent, and MessageEvent classes.
*/
function lazyUndici() {
return undici ??= require('internal/deps/undici/undici');
}

Activity

  1. bpasero commented on Aug 30, 2025

    @bpasero
    ContributorAuthor

    Oh, this was likely reported in #57649 already!

  2. joyeecheung commented on Sep 1, 2025

    @joyeecheung
    Member

    I think it comes from the fact that at evaluation time, the ESM built-in facade needs access all properties of the built-in and set them on the namespace object. I think https://github.com/tc39/proposal-defer-import-eval might be able to help with this

  3. bpasero commented on Sep 1, 2025

    @bpasero
    ContributorAuthor

    Yeah, deferred imports or just lazy import at the point of use helps us, but that makes one wonder what other core modules should be treated like this, would be great to get that documented somewhere.

  4. joyeecheung commented on Sep 1, 2025

    @joyeecheung
    Member

    All built-in modules are treated like this - all properties are eagerly evaluated and populated at evaluation for ESM, compared to in CommonJS things are a lot more dynamic and lazy. Not sure how detailed this should be documented though, because it is more of an implication of the ESM semantics that everything is static v.s. in CommonJS, things are a lot more dynamic, and somewhat implied by e.g. https://nodejs.org/api/esm.html#built-in-modules and the existence of https://nodejs.org/api/module.html#modulesyncbuiltinesmexports - I guess a note can be added to remind users that built-in loaded from ESM is static and always eagerly populated in entirety.

  5. added
    docIssues and PRs related to Node.js documentation.
    good first issueIssues that are suitable for first-time contributors.
    on Sep 1, 2025
  6. bpasero commented on Sep 1, 2025

    @bpasero
    ContributorAuthor

    I guess a note can be added to remind users that built-in loaded from ESM is static and always eagerly populated in entirety.

    That, but maybe also a call out to certain internal modules that do heavy lifting when imported like this, or even for common JS when certain properties are accessed.

  7. deepak1556 commented on Sep 1, 2025

    @deepak1556
    Contributor

    I think it comes from the fact that at evaluation time, the ESM built-in facade needs access all properties of the built-in and set them on the namespace object.

    Yup, builtinStrategy -> getESMFacade -> syncExports

    Image Image
  8. joyeecheung commented on Sep 1, 2025

    @joyeecheung
    Member

    but maybe also a call out to certain internal modules that do heavy lifting when imported like this

    That might be a bit too much implementation detail to document and can easily get out of sync as people try to optimize it e.g. slicing undici up or something

  9. axecalibur-dev commented on Sep 16, 2025

    @axecalibur-dev

    I'd like to take up the issue.

  10. ayush-0513 commented on Sep 21, 2025

    @ayush-0513

    Thanks for the explanation — that makes sense now! From what I saw in my benchmarks, the slowdown with import is just because ESM eagerly evaluates all properties in the built-in facades, while require stays lazy.

    In practice:

    Servers don’t really feel it since the cost is one-time.

    Small scripts or CLIs can feel slower (~8–10 ms difference), so require or createRequire is still better there.

    Tried a few workarounds (dynamic import(), caching, workers), they help but don’t fully remove the overhead.

    So for now it looks like: use import for long-running apps, stick with require if startup time matters.

    I'd like to take up the issue

  11. tysoncung commented on Oct 7, 2025

    @tysoncung

    Performance issues can be tricky. Have you profiled the code to identify bottlenecks? I'd be interested in helping optimize this if you can share some performance metrics or benchmarks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    docIssues and PRs related to Node.js documentation.good first issueIssues that are suitable for first-time contributors.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions