Skip to content

perf: eliminate conditional check in json strict mode hot path#651

Merged
Phillip9587 merged 1 commit intoexpressjs:masterfrom
Phillip9587:perf-json-strict-hot-path
Apr 29, 2026
Merged

perf: eliminate conditional check in json strict mode hot path#651
Phillip9587 merged 1 commit intoexpressjs:masterfrom
Phillip9587:perf-json-strict-hot-path

Conversation

@Phillip9587
Copy link
Copy Markdown
Member

This change refactors the JSON parser to eliminate the per-request if (strict) conditional check from the parsing hot path.

Instead of checking strict mode during parsing, we now create specialized parser functions at middleware initialization based on the strict option.

I'm aware that this introduces some code duplication between the strict and non-strict parser implementations, but I think that is a necessary trade-off.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR refactors the JSON parser logic by extracting the parse function into a separate helper function createJsonParser. The refactoring improves code organization by separating parser creation from middleware setup, and fixes an incorrect JSDoc return type annotation.

Key Changes

  • Extracted inline parse function into a new createJsonParser helper function that returns the appropriate parser based on the strict option
  • Corrected JSDoc return type annotation for firstchar function from {function} to {string | undefined}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/types/json.js
Comment thread lib/types/json.js
@Phillip9587
Copy link
Copy Markdown
Member Author

Ref: expressjs/perf-wg#77

A (Baseline):

npm run test:load -- --test=@expressjs/perf-load-body-parser --write

B (This branch):

npm run test:load -- --test=@expressjs/perf-load-body-parser --write --overrides='{"body-parser":"Phillip9587/body-parser#perf-json-strict-hot-path"}'

My local load test results:

A Results: ./perf/load/body-parser/results/result-1764606166571.json
====================================================================

┌─────────┬────────┬────────┬─────────┬─────────┬───────────┬──────────┬─────────┐
│ Stat    │ 2.5%   │ 50%    │ 97.5%   │ 99%     │ Avg       │ Stdev    │ Max     │
├─────────┼────────┼────────┼─────────┼─────────┼───────────┼──────────┼─────────┤
│ Latency │ 678 ms │ 724 ms │ 1003 ms │ 1202 ms │ 753.07 ms │ 96.18 ms │ 1901 ms │
└─────────┴────────┴────────┴─────────┴─────────┴───────────┴──────────┴─────────┘
┌───────────┬────────┬─────────┬─────────┬─────────┬───────────┬──────────┬────────┐
│ Stat      │ 1%     │ 2.5%    │ 50%     │ 97.5%   │ Avg       │ Stdev    │ Min    │
├───────────┼────────┼─────────┼─────────┼─────────┼───────────┼──────────┼────────┤
│ Req/Sec   │ 458    │ 10,335  │ 19,951  │ 21,423  │ 19,303.24 │ 3,059.32 │ 458    │
├───────────┼────────┼─────────┼─────────┼─────────┼───────────┼──────────┼────────┤
│ Bytes/Sec │ 322 kB │ 7.54 MB │ 14.5 MB │ 15.6 MB │ 14.1 MB   │ 2.23 MB  │ 321 kB │
└───────────┴────────┴─────────┴─────────┴─────────┴───────────┴──────────┴────────┘

Req/Bytes counts sampled once per second.
# of samples: 60

1173k requests in 60.03s, 844 MB read
83 errors (0 timeouts)

B Results: ./perf/load/body-parser/results/result-1764606062525.json
====================================================================

┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬─────────┐
│ Stat    │ 2.5%   │ 50%    │ 97.5%  │ 99%    │ Avg       │ Stdev    │ Max     │
├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼─────────┤
│ Latency │ 594 ms │ 609 ms │ 844 ms │ 970 ms │ 628.79 ms │ 66.41 ms │ 1548 ms │
└─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴─────────┘
┌───────────┬────────┬─────────┬─────────┬─────────┬───────────┬──────────┬────────┐
│ Stat      │ 1%     │ 2.5%    │ 50%     │ 97.5%   │ Avg       │ Stdev    │ Min    │
├───────────┼────────┼─────────┼─────────┼─────────┼───────────┼──────────┼────────┤
│ Req/Sec   │ 409    │ 17,279  │ 21,295  │ 21,727  │ 20,632.29 │ 2,789.21 │ 409    │
├───────────┼────────┼─────────┼─────────┼─────────┼───────────┼──────────┼────────┤
│ Bytes/Sec │ 307 kB │ 12.6 MB │ 15.5 MB │ 15.8 MB │ 15 MB     │ 2.03 MB  │ 307 kB │
└───────────┴────────┴─────────┴─────────┴─────────┴───────────┴──────────┴────────┘

Req/Bytes counts sampled once per second.
# of samples: 60

1251k requests in 60.02s, 902 MB read
4 errors (0 timeouts)

Comparison: B Wins
==================
{
  diff: { rps: '-6.44%', throughput: '-6.44%', latency: '19.76%' },
  a: {
    avgRPS: 19303.24,
    avgThroughput: 14071464.54,
    avgLatency: 753.07,
    status: { '200': { count: 1158142 } }
  },
  b: {
    avgRPS: 20632.29,
    avgThroughput: 15040637.87,
    avgLatency: 628.79,
    status: { '200': { count: 1237872 } }
  }
}

cc: @wesleytodd

@wesleytodd
Copy link
Copy Markdown
Member

Ah awesome, yeah the perf tests are still a work in progress. So there is inconsistencies between runs and sometimes you may want to change the test requests or server implementation a bit to get real results. I just threw together some low effort requests with bodies in that PR but feel free to write ones that show the perf improvement even more than this did on your machine. And any feedback on running these is welcome!

Refactor JSON parser creation to return specialized parser functions
based on strict mode setting, removing the per-request `if (strict)`
conditional check in the parsing hot path.
@Phillip9587
Copy link
Copy Markdown
Member Author

Hey @UlisesGascon @jonchurch 👋
Just revisited this PR and wanted to give it another gentle bump - it’s been a little while, and I’d love to get your feedback whenever you get a chance. Appreciate your time! 😊

@Phillip9587
Copy link
Copy Markdown
Member Author

Hey @UlisesGascon @jonchurch. A review would be appreciated

@Phillip9587
Copy link
Copy Markdown
Member Author

@UlisesGascon @jonchurch I plan to merge this next week. Maybe you have some time to check this out.

Copy link
Copy Markdown
Member

@jonchurch jonchurch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

willing to give it a shot, if the duplication is still bothering you later we can land this where we extract out the biggest chunk that doesnt change per parser (make a parseBody function):

function parseBody (body, reviver) {
  try {
    debug('parse json')
    return JSON.parse(body, reviver)
  } catch (e) {
    throw normalizeJsonSyntaxError(e, {
      message: e.message,
      stack: e.stack
    })
  }
}

/**
 * Create a JSON parse function
 *
 * @param {object} [options]
 * @return {function}
 * @private
 */
function createJsonParser (options) {
  const reviver = options?.reviver
  const strict = options?.strict !== false

  if (strict) {
    return function parseStrict (body) {
      if (body.length === 0) {
        // special-case empty json body, as it's a common client-side mistake
        // TODO: maybe make this configurable or part of "strict" option
        return {}
      }

      const first = firstchar(body)
      if (first !== '{' && first !== '[') {
        debug('strict violation')
        throw createStrictSyntaxError(body, first)
      }

      return parseBody(body, reviver)
    }
  }

  return function parse (body) {
    if (body.length === 0) {
      // special-case empty json body, as it's a common client-side mistake
      // TODO: maybe make this configurable or part of "strict" option
      return {}
    }

    return parseBody(body, reviver)
  }
}

@Phillip9587
Copy link
Copy Markdown
Member Author

willing to give it a shot, if the duplication is still bothering you later we can land this where we extract out the biggest chunk that doesnt change per parser (make a parseBody function):

Let's maybe add this in a follow up PR.

@Phillip9587 Phillip9587 merged commit 0018adf into expressjs:master Apr 29, 2026
16 checks passed
@Phillip9587 Phillip9587 deleted the perf-json-strict-hot-path branch April 29, 2026 20:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants