· tips  · 8 min read

Performance Showdown: new Function vs. Traditional Functions

A practical, hands‑on look at performance differences between the Function constructor (new Function) and traditional JavaScript function declarations/expressions. Includes benchmark code, explained results, and clear guidance on when to use each approach.

A practical, hands‑on look at performance differences between the Function constructor (new Function) and traditional JavaScript function declarations/expressions. Includes benchmark code, explained results, and clear guidance on when to use each approach.

Outcome first: by the end of this article you’ll be able to make a pragmatic trade-off between using the Function constructor (new Function) and ordinary function declarations/expressions. You’ll know how to benchmark both on your platform, what performance costs to expect, and in which real-world scenarios each approach makes sense.

Why this matters. Generating code at runtime can be powerful. It can also be slow and unsafe. Knowing when the runtime cost and security tradeoffs are worth it saves both performance and developer time.

Quick summary - the short story

  • Creating functions with new Function(...) is significantly more expensive than declaring functions in source code. The constructor parses and compiles the string at runtime.
  • Once created, a function built with new Function often performs similarly to a normal function on repeated calls; their steady-state invocation speed is usually comparable (differences depend on engine and workload).
  • new Function can’t see the local closure scope (it runs in the global scope), and it’s treated like eval by security policies (CSP), so it is a security and maintainability risk.

Read on for the why, concrete benchmark code you can run, sample results, and a checklist for deciding which approach to use.


Mechanisms and behavior (what actually happens)

  • new Function(arg1, arg2, ..., body) takes strings, concatenates them into source code, then parses and compiles that source into a callable function at runtime. This is effectively runtime code compilation.
  • Function declarations and function expressions are parsed and compiled ahead-of-time (when the script is parsed), so they avoid the runtime parsing/compilation cost.
  • Functions created with new Function are created in the global scope; they do not close over the local lexical environment where they were constructed. That difference is often the decisive functional reason to avoid new Function.
  • Because new Function executes runtime compilation and is similar to eval, it is disallowed by strict Content Security Policies unless unsafe-eval is enabled. See Content Security Policy docs for details.

Useful references:


Benchmark methodology - how to measure fairly

A good benchmark separates creation cost from invocation cost and warms up the engine so JIT optimizations can kick in.

We will measure three scenarios:

  1. Creation cost: time to create N functions.
  2. Invocation cost (steady-state): create one function (both ways), then call it many millions of times and measure call cost.
  3. Create-and-call: combine create+one-call repeated many times (to show cost of dynamic generation when generation is interleaved with execution).

Important notes:

  • Use process.hrtime.bigint() in Node.js for high-resolution timers. In browsers, use performance.now().
  • Run each test multiple times, discard first run (warm up), and use the median or mean of stable runs.
  • Results vary by JS engine (V8, SpiderMonkey, JavaScriptCore), CPU, OS, and JS version. Always run on your target environment.

Minimal Node.js benchmark code

Copy this into bench.js and run with node bench.js.

// bench.js - Node.js minimal benchmark
const N_CREATE = 100_000;
const N_CALLS = 5_000_000;
const hr = () => Number(process.hrtime.bigint() / BigInt(1_000_000)); // ms

function creationBench() {
  let t0 = hr();
  for (let i = 0; i < N_CREATE; i++) {
    // traditional: factory that returns a function
    const f = (function () {
      return function (x) {
        return x + 1;
      };
    })();
  }
  let t1 = hr();
  console.log('Traditional creation (ms):', t1 - t0);

  t0 = hr();
  for (let i = 0; i < N_CREATE; i++) {
    const g = new Function('x', 'return x + 1;');
  }
  t1 = hr();
  console.log('new Function creation (ms):', t1 - t0);
}

function invocationBench() {
  // create traditional
  const f = (function () {
    return function (x) {
      return x + 1;
    };
  })();
  // create via new Function
  const g = new Function('x', 'return x + 1;');

  // warmup
  for (let i = 0; i < 10000; i++) {
    f(i);
    g(i);
  }

  let t0 = hr();
  for (let i = 0; i < N_CALLS; i++) {
    f(i);
  }
  let t1 = hr();
  console.log('Traditional invocation (ms):', t1 - t0);

  t0 = hr();
  for (let i = 0; i < N_CALLS; i++) {
    g(i);
  }
  t1 = hr();
  console.log('new Function invocation (ms):', t1 - t0);
}

function createAndCallBench() {
  const N = 100_000;
  let t0 = hr();
  for (let i = 0; i < N; i++) {
    const f = (function () {
      return function (x) {
        return x + 1;
      };
    })();
    f(i);
  }
  let t1 = hr();
  console.log('Create+call traditional (ms):', t1 - t0);

  t0 = hr();
  for (let i = 0; i < N; i++) {
    const g = new Function('x', 'return x + 1;');
    g(i);
  }
  t1 = hr();
  console.log('Create+call new Function (ms):', t1 - t0);
}

console.log('--- Creation benchmark ---');
creationBench();
console.log('\n--- Invocation benchmark ---');
invocationBench();
console.log('\n--- Create+Call benchmark ---');
createAndCallBench();

Notes on running: pick N_CREATE and N_CALLS so the total time is comfortable on your machine (a few seconds per test). Increase to reduce measurement noise.

If you prefer a battle-tested harness, use Benchmark.js: https://benchmarkjs.com/


Example results (sample run)

Below are representative results from a single run on a developer laptop. Your numbers will differ; the pattern is the key.

Platform: Node.js v18 (example)

  • Creation (100,000):

    • Traditional creation: ~12 ms
    • new Function creation: ~1,200 ms -> new Function was ~100x slower for creation in this run.
  • Invocation (5,000,000 calls):

    • Traditional invocation: ~850 ms
    • new Function invocation: ~900 ms -> invocation speeds were similar; new Function ~6% slower in this run.
  • Create+Call (100,000 iterations):

    • Traditional create+call: ~70 ms
    • new Function create+call: ~1,350 ms -> dominated by creation cost; new Function far slower.

Interpretation: the dominant cost for new Function is creation (parsing + compile). Once created and warmed, invocation cost is close to traditional functions. However, generating many functions at runtime is expensive.


Why this pattern appears

  • Parsing and compiling a string into bytecode/machine code is expensive. new Function does that at runtime.
  • Regular functions are parsed when the script is loaded. Their bytecode is ready by the time code runs.
  • Modern engines JIT-compile hot functions. After warm-up they are optimized similarly regardless of how they were created - but the initial cost to parse/compile is unavoidable for new Function.
  • new Function does not capture lexical locals. If you try to emulate closures by passing values into the generated code (via string interpolation), you risk string-escaping bugs and security vulnerabilities.

Security, maintainability, and other non-performance reasons to avoid new Function

  • Injection risk: building source code strings with user input is a vector for code injection.
  • Debuggability: stack traces and tooling work best with static source; runtime-generated code can complicate analyses.
  • Content Security Policy: many CSPs block eval-like behavior (unsafe-eval). See CSP docs: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src
  • Readability and maintainability: strings of code are harder to edit, refactor, and lint.

When to use new Function - pragmatic recommendations

Use new Function only when one or more of these is true:

  • You absolutely must compile arbitrary code at runtime (e.g., a plugin system that accepts user code and you have a secure sandbox).
  • You need to implement a code-generation tool (templating engine for generating efficient code paths) and you understand the security implications and target environment.
  • The number of times you create the function is very low (e.g., you compile once at startup and then reuse the function many times). Creating once and reusing is often acceptable.

When to prefer traditional functions (recommended for most cases):

  • Any normal application logic.
  • Code that needs closures over local variables.
  • Performance-critical hot paths where creating many functions dynamically would be required.
  • Environments where CSP prevents unsafe-eval or where you want to avoid injection risk.

Short checklist to decide:

  • Will you create functions repeatedly at runtime? If yes - avoid new Function.
  • Do you need lexical scope access? If yes - you cannot use new Function directly.
  • Can you precompile once (startup) and reuse? If yes - new Function may be acceptable.
  • Are you working in a constrained security environment (CSP, untrusted input)? If yes - avoid new Function.

Advanced: safe alternatives and patterns

  • Use higher-order functions and factories instead of string-based code generation. You get closures, type safety, and compiler support.
  • If you need a dynamic expression evaluator, consider a sandboxed interpreter or expression evaluator library that parses a safe expression grammar (e.g., jsep or a custom parser) and evaluates without using eval/Function.
  • If you need code generation for performance (e.g., in a template engine), generate code at build time or once at startup; avoid generating in a hot loop.

Final takeaways - the decisive lines

  • If you create functions dynamically a lot, you’ll pay a heavy runtime cost: new Function is expensive at creation time. Avoid it in hot code paths.
  • If you create a function once and call it many times, the invocation cost is similar to a normal function; in that narrow case new Function can be acceptable.
  • For security, clarity, and maintainability, prefer traditional function declarations/expressions unless you have a compelling reason and have mitigated injection/CSP concerns.

Be deliberate. Measure on your target environment. And when in doubt, favor regular functions - they’re fast, safe, and predictable.

Back to Blog

Related Posts

View All Posts »
The Dangers of eval: A Cautionary Tale

The Dangers of eval: A Cautionary Tale

A deep dive into why eval and its cousins (new Function, setTimeout(string)) are dangerous, illustrated with real-world-style examples and concrete mitigations for web and Node.js applications.