Skip to content

Conversation

@cesco69
Copy link
Contributor

@cesco69 cesco69 commented Jan 15, 2026

Inline generated serializers to remove anonymous functions

This PR removes the generation of per-schema anonymous serializer functions and fully inlines all serialization logic into a single main function.

Eg. this schema:

{
  type: 'object',
  properties: {
    firstName: { type: 'string' },
    lastName: { type: 'string' },
    age: { type: 'integer' },
    address: {
      type: 'object',
      properties: {
        street: { type: 'string' },
        city: { type: 'string' },
        zip: { type: 'string' }
      }
    },
    hobbies: {
      type: 'array',
      items: { type: 'string' }
    }
  }
}
  • Each nested schema (address, hobbies, root) generated its own anonymous serializer function.
  • The root serializer (anonymous0) delegated work via function calls (anonymous1, anonymous2).
  • Serialization flow = function calls + stack frames

Actually generate this output code:

Show
const {
  asString,
  asNumber,
  asBoolean,
  asDateTime,
  asDate,
  asTime,
  asUnsafeString
} = serializer

const asInteger = serializer.asInteger.bind(serializer)

const JSON_STR_BEGIN_OBJECT = '{'
const JSON_STR_END_OBJECT = '}'
const JSON_STR_BEGIN_ARRAY = '['
const JSON_STR_END_ARRAY = ']'
const JSON_STR_COMMA = ','
const JSON_STR_COLONS = ':'
const JSON_STR_QUOTE = '"'
const JSON_STR_EMPTY_OBJECT = JSON_STR_BEGIN_OBJECT + JSON_STR_END_OBJECT
const JSON_STR_EMPTY_ARRAY = JSON_STR_BEGIN_ARRAY + JSON_STR_END_ARRAY
const JSON_STR_EMPTY_STRING = JSON_STR_QUOTE + JSON_STR_QUOTE
const JSON_STR_NULL = 'null'



// #/properties/address
function anonymous1 (input) {
  const obj = (input && typeof input.toJSON === 'function')
? input.toJSON()
: input

  if (obj === null) return JSON_STR_EMPTY_OBJECT

  let json = JSON_STR_BEGIN_OBJECT

let addComma = false

  const value_street_0 = obj["street"]
  if (value_street_0 !== undefined) {
    !addComma && (addComma = true) || (json += JSON_STR_COMMA)
    json += "\"street\":"
    
    if (typeof value_street_0 !== 'string') {
      if (value_street_0 === null) {
        json += JSON_STR_EMPTY_STRING
      } else if (value_street_0 instanceof Date) {
        json += JSON_STR_QUOTE + value_street_0.toISOString() + JSON_STR_QUOTE
      } else if (value_street_0 instanceof RegExp) {
        json += asString(value_street_0.source)
      } else {
        json += asString(value_street_0.toString())
      }
    } else {
      json += asString(value_street_0)
    }
    
  }

  const value_city_1 = obj["city"]
  if (value_city_1 !== undefined) {
    !addComma && (addComma = true) || (json += JSON_STR_COMMA)
    json += "\"city\":"
    
    if (typeof value_city_1 !== 'string') {
      if (value_city_1 === null) {
        json += JSON_STR_EMPTY_STRING
      } else if (value_city_1 instanceof Date) {
        json += JSON_STR_QUOTE + value_city_1.toISOString() + JSON_STR_QUOTE
      } else if (value_city_1 instanceof RegExp) {
        json += asString(value_city_1.source)
      } else {
        json += asString(value_city_1.toString())
      }
    } else {
      json += asString(value_city_1)
    }
    
  }

  const value_zip_2 = obj["zip"]
  if (value_zip_2 !== undefined) {
    !addComma && (addComma = true) || (json += JSON_STR_COMMA)
    json += "\"zip\":"
    
    if (typeof value_zip_2 !== 'string') {
      if (value_zip_2 === null) {
        json += JSON_STR_EMPTY_STRING
      } else if (value_zip_2 instanceof Date) {
        json += JSON_STR_QUOTE + value_zip_2.toISOString() + JSON_STR_QUOTE
      } else if (value_zip_2 instanceof RegExp) {
        json += asString(value_zip_2.source)
      } else {
        json += asString(value_zip_2.toString())
      }
    } else {
      json += asString(value_zip_2)
    }
    
  }

return json + JSON_STR_END_OBJECT

}


function anonymous2 (obj) {
  // #/properties/hobbies

if (obj === null) return JSON_STR_EMPTY_ARRAY
if (!Array.isArray(obj)) {
  throw new TypeError(`The value of '#/properties/hobbies' does not match schema definition.`)
}
const arrayLength = obj.length

const arrayEnd = arrayLength - 1
let json = ''

  for (let i = 0; i < arrayLength; i++) {
    if (i) {
      json += JSON_STR_COMMA
    }
    const value = obj[i]
    
    if (typeof value !== 'string') {
      if (value === null) {
        json += JSON_STR_EMPTY_STRING
      } else if (value instanceof Date) {
        json += JSON_STR_QUOTE + value.toISOString() + JSON_STR_QUOTE
      } else if (value instanceof RegExp) {
        json += asString(value.source)
      } else {
        json += asString(value.toString())
      }
    } else {
      json += asString(value)
    }
    
  }
return JSON_STR_BEGIN_ARRAY + json + JSON_STR_END_ARRAY

}

// #
function anonymous0 (input) {
  const obj = (input && typeof input.toJSON === 'function')
? input.toJSON()
: input

  if (obj === null) return JSON_STR_EMPTY_OBJECT

  let json = JSON_STR_BEGIN_OBJECT

let addComma = false

  const value_firstName_0 = obj["firstName"]
  if (value_firstName_0 !== undefined) {
    !addComma && (addComma = true) || (json += JSON_STR_COMMA)
    json += "\"firstName\":"
    
    if (typeof value_firstName_0 !== 'string') {
      if (value_firstName_0 === null) {
        json += JSON_STR_EMPTY_STRING
      } else if (value_firstName_0 instanceof Date) {
        json += JSON_STR_QUOTE + value_firstName_0.toISOString() + JSON_STR_QUOTE
      } else if (value_firstName_0 instanceof RegExp) {
        json += asString(value_firstName_0.source)
      } else {
        json += asString(value_firstName_0.toString())
      }
    } else {
      json += asString(value_firstName_0)
    }
    
  }

  const value_lastName_1 = obj["lastName"]
  if (value_lastName_1 !== undefined) {
    !addComma && (addComma = true) || (json += JSON_STR_COMMA)
    json += "\"lastName\":"
    
    if (typeof value_lastName_1 !== 'string') {
      if (value_lastName_1 === null) {
        json += JSON_STR_EMPTY_STRING
      } else if (value_lastName_1 instanceof Date) {
        json += JSON_STR_QUOTE + value_lastName_1.toISOString() + JSON_STR_QUOTE
      } else if (value_lastName_1 instanceof RegExp) {
        json += asString(value_lastName_1.source)
      } else {
        json += asString(value_lastName_1.toString())
      }
    } else {
      json += asString(value_lastName_1)
    }
    
  }

  const value_age_2 = obj["age"]
  if (value_age_2 !== undefined) {
    !addComma && (addComma = true) || (json += JSON_STR_COMMA)
    json += "\"age\":"
    json += asInteger(value_age_2)
  }

  const value_address_3 = obj["address"]
  if (value_address_3 !== undefined) {
    !addComma && (addComma = true) || (json += JSON_STR_COMMA)
    json += "\"address\":"
    json += anonymous1(value_address_3)
  }

  const value_hobbies_4 = obj["hobbies"]
  if (value_hobbies_4 !== undefined) {
    !addComma && (addComma = true) || (json += JSON_STR_COMMA)
    json += "\"hobbies\":"
    json += anonymous2(value_hobbies_4)
  }

return json + JSON_STR_END_OBJECT

}

const main = anonymous0
return main

With this PR the output become

  • A single main function is generated.
  • All nested object/array serialization logic is fully inlined.
  • No extra anonymous functions, no cross-function calls.
  • Serialization flow = straight-line code with scoped locals.
Show
const {
  asString,
  asNumber,
  asBoolean,
  asDateTime,
  asDate,
  asTime,
  asUnsafeString
} = serializer

const asInteger = serializer.asInteger.bind(serializer)

const JSON_STR_BEGIN_OBJECT = '{'
const JSON_STR_END_OBJECT = '}'
const JSON_STR_BEGIN_ARRAY = '['
const JSON_STR_END_ARRAY = ']'
const JSON_STR_COMMA = ','
const JSON_STR_COLONS = ':'
const JSON_STR_QUOTE = '"'
const JSON_STR_EMPTY_OBJECT = JSON_STR_BEGIN_OBJECT + JSON_STR_END_OBJECT
const JSON_STR_EMPTY_ARRAY = JSON_STR_BEGIN_ARRAY + JSON_STR_END_ARRAY
const JSON_STR_EMPTY_STRING = JSON_STR_QUOTE + JSON_STR_QUOTE
const JSON_STR_NULL = 'null'

function main(input) {
let json = ''

const obj_0 = (input && typeof input.toJSON === 'function')
? input.toJSON()
: input

if (obj_0 === null) {
json += JSON_STR_EMPTY_OBJECT
} else {
json += JSON_STR_BEGIN_OBJECT
let addComma_1 = false

const value_firstName_2 = obj_0["firstName"]
if (value_firstName_2 !== undefined) {
  !addComma_1 && (addComma_1 = true) || (json += JSON_STR_COMMA)
  json += "\"firstName\":"

  if (typeof value_firstName_2 !== 'string') {
    if (value_firstName_2 === null) {
      json += JSON_STR_EMPTY_STRING
    } else if (value_firstName_2 instanceof Date) {
      json += JSON_STR_QUOTE + value_firstName_2.toISOString() + JSON_STR_QUOTE
    } else if (value_firstName_2 instanceof RegExp) {
      json += asString(value_firstName_2.source)
    } else {
      json += asString(value_firstName_2.toString())
    }
  } else {
    json += asString(value_firstName_2)
  }

}

const value_lastName_3 = obj_0["lastName"]
if (value_lastName_3 !== undefined) {
  !addComma_1 && (addComma_1 = true) || (json += JSON_STR_COMMA)
  json += "\"lastName\":"

  if (typeof value_lastName_3 !== 'string') {
    if (value_lastName_3 === null) {
      json += JSON_STR_EMPTY_STRING
    } else if (value_lastName_3 instanceof Date) {
      json += JSON_STR_QUOTE + value_lastName_3.toISOString() + JSON_STR_QUOTE
    } else if (value_lastName_3 instanceof RegExp) {
      json += asString(value_lastName_3.source)
    } else {
      json += asString(value_lastName_3.toString())
    }
  } else {
    json += asString(value_lastName_3)
  }

}

const value_age_4 = obj_0["age"]
if (value_age_4 !== undefined) {
  !addComma_1 && (addComma_1 = true) || (json += JSON_STR_COMMA)
  json += "\"age\":"
  json += asInteger(value_age_4)
}

const value_address_5 = obj_0["address"]
if (value_address_5 !== undefined) {
  !addComma_1 && (addComma_1 = true) || (json += JSON_STR_COMMA)
  json += "\"address\":"

  const obj_6 = (value_address_5 && typeof value_address_5.toJSON === 'function')
    ? value_address_5.toJSON()
    : value_address_5

  if (obj_6 === null) {
    json += JSON_STR_EMPTY_OBJECT
  } else {
    json += JSON_STR_BEGIN_OBJECT
    let addComma_7 = false

    const value_street_8 = obj_6["street"]
    if (value_street_8 !== undefined) {
      !addComma_7 && (addComma_7 = true) || (json += JSON_STR_COMMA)
      json += "\"street\":"

      if (typeof value_street_8 !== 'string') {
        if (value_street_8 === null) {
          json += JSON_STR_EMPTY_STRING
        } else if (value_street_8 instanceof Date) {
          json += JSON_STR_QUOTE + value_street_8.toISOString() + JSON_STR_QUOTE
        } else if (value_street_8 instanceof RegExp) {
          json += asString(value_street_8.source)
        } else {
          json += asString(value_street_8.toString())
        }
      } else {
        json += asString(value_street_8)
      }

    }

    const value_city_9 = obj_6["city"]
    if (value_city_9 !== undefined) {
      !addComma_7 && (addComma_7 = true) || (json += JSON_STR_COMMA)
      json += "\"city\":"

      if (typeof value_city_9 !== 'string') {
        if (value_city_9 === null) {
          json += JSON_STR_EMPTY_STRING
        } else if (value_city_9 instanceof Date) {
          json += JSON_STR_QUOTE + value_city_9.toISOString() + JSON_STR_QUOTE
        } else if (value_city_9 instanceof RegExp) {
          json += asString(value_city_9.source)
        } else {
          json += asString(value_city_9.toString())
        }
      } else {
        json += asString(value_city_9)
      }

    }

    const value_zip_10 = obj_6["zip"]
    if (value_zip_10 !== undefined) {
      !addComma_7 && (addComma_7 = true) || (json += JSON_STR_COMMA)
      json += "\"zip\":"

      if (typeof value_zip_10 !== 'string') {
        if (value_zip_10 === null) {
          json += JSON_STR_EMPTY_STRING
        } else if (value_zip_10 instanceof Date) {
          json += JSON_STR_QUOTE + value_zip_10.toISOString() + JSON_STR_QUOTE
        } else if (value_zip_10 instanceof RegExp) {
          json += asString(value_zip_10.source)
        } else {
          json += asString(value_zip_10.toString())
        }
      } else {
        json += asString(value_zip_10)
      }

    }

    json += JSON_STR_END_OBJECT

  }

}

const value_hobbies_11 = obj_0["hobbies"]
if (value_hobbies_11 !== undefined) {
  !addComma_1 && (addComma_1 = true) || (json += JSON_STR_COMMA)
  json += "\"hobbies\":"

  const obj_12 = value_hobbies_11
  if (obj_12 === null) {
    json += JSON_STR_EMPTY_ARRAY
  } else if (!Array.isArray(obj_12)) {
    throw new TypeError(`The value of '#/properties/hobbies' does not match schema definition.`)
  } else {
    const arrayLength_obj_12 = obj_12.length

    json += JSON_STR_BEGIN_ARRAY

    for (let i = 0; i < arrayLength_obj_12; i++) {
      if (i) {
        json += JSON_STR_COMMA
      }
      const value = obj_12[i]

      if (typeof value !== 'string') {
        if (value === null) {
          json += JSON_STR_EMPTY_STRING
        } else if (value instanceof Date) {
          json += JSON_STR_QUOTE + value.toISOString() + JSON_STR_QUOTE
        } else if (value instanceof RegExp) {
          json += asString(value.source)
        } else {
          json += asString(value.toString())
        }
      } else {
        json += asString(value)
      }

    }
    json += JSON_STR_END_ARRAY
  }
}

json += JSON_STR_END_OBJECT

}

return json
}

return main

This avoids:

  • Call overhead
  • Stack frame creation
  • De-optimizations in tight loops
  • This is especially relevant for high-throughput serialization.

Better JIT optimization opportunities

Inlining allows the JS engine to:

  • Perform constant folding across nested scopes
  • Keep json and addComma in registers
  • Avoid polymorphic call sites

With separate functions, V8 often cannot inline aggressively due to:

  • Closure boundaries
  • Indirect references
  • Different argument shapes

Reduced memory pressure:

  • No extra function objects allocated
  • No closures capturing shared constants
  • Fewer references held alive by the serializer

Benchmark

image
npm run bench:cmp

> [email protected] bench:cmp
> node ./benchmark/bench-cmp-branch.js

Select the branch you want to compare (feature branch):
patch-1
Select the branch you want to compare with (main branch):
main
Checking out "patch-1"
Execute "npm run bench"

> [email protected] bench
> node --expose-gc ./benchmark/bench.js

short string............................................. x 9,804,722 ops/sec ±0.13% (10866143 runs sampled)
unsafe short string...................................... x 14,227,602 ops/sec ±0.10% (18759176 runs sampled)
short string with double quote........................... x 9,907,576 ops/sec ±0.16% (10308550 runs sampled)
long string without double quotes........................ x 57,994 ops/sec ±0.27% (56299 runs sampled)
unsafe long string without double quotes................. x 16,374,469 ops/sec ±0.10% (21479675 runs sampled)
long string.............................................. x 48,778 ops/sec ±0.29% (47422 runs sampled)
unsafe long string....................................... x 15,659,637 ops/sec ±0.10% (20580504 runs sampled)
number................................................... x 16,544,424 ops/sec ±0.10% (21791805 runs sampled)
integer.................................................. x 14,119,653 ops/sec ±0.09% (18634650 runs sampled)
formatted date-time...................................... x 1,228,877 ops/sec ±0.14% (1210968 runs sampled)
formatted date........................................... x 1,040,631 ops/sec ±0.30% (1007657 runs sampled)
formatted time........................................... x 1,040,350 ops/sec ±0.23% (1011087 runs sampled)
short array of numbers................................... x 68,091 ops/sec ±0.40% (65252 runs sampled)
short array of integers.................................. x 68,360 ops/sec ±0.39% (65566 runs sampled)
short array of short strings............................. x 20,939 ops/sec ±0.43% (20347 runs sampled)
short array of long strings.............................. x 21,257 ops/sec ±0.41% (20694 runs sampled)
short array of objects with properties of different types x 10,563 ops/sec ±0.65% (10152 runs sampled)
object with number property.............................. x 15,258,682 ops/sec ±0.10% (20147978 runs sampled)
object with integer property............................. x 14,260,632 ops/sec ±0.18% (18765438 runs sampled)
object with short string property........................ x 10,769,506 ops/sec ±0.11% (13103421 runs sampled)
object with long string property......................... x 49,007 ops/sec ±0.27% (47785 runs sampled)
object with properties of different types................ x 2,042,713 ops/sec ±0.18% (1904323 runs sampled)
simple object............................................ x 7,773,937 ops/sec ±0.73% (6345145 runs sampled)
simple object with required fields....................... x 8,065,693 ops/sec ±0.80% (6729141 runs sampled)
object with const string property........................ x 16,548,329 ops/sec ±0.09% (21769208 runs sampled)
object with const number property........................ x 16,620,515 ops/sec ±0.09% (21802933 runs sampled)
object with const bool property.......................... x 16,158,006 ops/sec ±0.09% (21246100 runs sampled)
object with const object property........................ x 13,855,161 ops/sec ±0.10% (18239971 runs sampled)
object with const null property.......................... x 16,427,059 ops/sec ±0.12% (21537322 runs sampled)

Checking out "main"
Execute "npm run bench"

> [email protected] bench
> node --expose-gc ./benchmark/bench.js

short string............................................. x 10,522,743 ops/sec ±0.19% (12469976 runs sampled)
unsafe short string...................................... x 16,398,294 ops/sec ±0.12% (21491002 runs sampled)
short string with double quote........................... x 9,949,116 ops/sec ±0.18% (10360268 runs sampled)
long string without double quotes........................ x 57,833 ops/sec ±0.30% (55941 runs sampled)
unsafe long string without double quotes................. x 13,398,052 ops/sec ±0.10% (17503034 runs sampled)
long string.............................................. x 48,597 ops/sec ±0.30% (47111 runs sampled)
unsafe long string....................................... x 13,315,453 ops/sec ±0.09% (17381390 runs sampled)
number................................................... x 14,180,732 ops/sec ±0.08% (18724991 runs sampled)
integer.................................................. x 13,495,527 ops/sec ±0.09% (17640585 runs sampled)
formatted date-time...................................... x 885,279 ops/sec ±0.23% (796834 runs sampled)
formatted date........................................... x 716,671 ops/sec ±0.31% (659055 runs sampled)
formatted time........................................... x 730,594 ops/sec ±0.24% (668323 runs sampled)
short array of numbers................................... x 49,147 ops/sec ±0.61% (44499 runs sampled)
short array of integers.................................. x 49,745 ops/sec ±0.59% (45621 runs sampled)
short array of short strings............................. x 13,473 ops/sec ±0.62% (12650 runs sampled)
short array of long strings.............................. x 15,641 ops/sec ±0.65% (14281 runs sampled)
short array of objects with properties of different types x 8,950 ops/sec ±0.94% (8138 runs sampled)
object with number property.............................. x 12,126,174 ops/sec ±0.11% (15677747 runs sampled)
object with integer property............................. x 13,004,555 ops/sec ±0.15% (17005326 runs sampled)
object with short string property........................ x 9,690,340 ops/sec ±0.11% (10158626 runs sampled)
object with long string property......................... x 35,943 ops/sec ±0.49% (33766 runs sampled)
object with properties of different types................ x 1,346,289 ops/sec ±0.58% (1238969 runs sampled)
simple object............................................ x 5,157,205 ops/sec ±0.85% (4380269 runs sampled)
simple object with required fields....................... x 5,309,242 ops/sec ±1.72% (4464184 runs sampled)
object with const string property........................ x 12,870,222 ops/sec ±0.11% (16840934 runs sampled)
object with const number property........................ x 12,167,883 ops/sec ±0.11% (15744556 runs sampled)
object with const bool property.......................... x 12,724,152 ops/sec ±0.11% (16629577 runs sampled)
object with const object property........................ x 12,499,170 ops/sec ±0.09% (16293815 runs sampled)
object with const null property.......................... x 12,687,708 ops/sec ±0.14% (16550049 runs sampled)

short string..............................................-6.82%
unsafe short string......................................-13.24%
short string with double quote............................-0.42%
long string without double quotes.........................+0.28%
unsafe long string without double quotes.................+22.22%
long string...............................................+0.37%
unsafe long string........................................+17.6%
number...................................................+16.67%
integer...................................................+4.62%
formatted date-time......................................+38.81%
formatted date............................................+45.2%
formatted time............................................+42.4%
short array of numbers...................................+38.55%
short array of integers..................................+37.42%
short array of short strings.............................+55.41%
short array of long strings..............................+35.91%
short array of objects with properties of different types+18.02%
object with number property..............................+25.83%
object with integer property..............................+9.66%
object with short string property........................+11.14%
object with long string property.........................+36.35%
object with properties of different types................+51.73%
simple object............................................+50.74%
simple object with required fields.......................+51.92%
object with const string property........................+28.58%
object with const number property........................+36.59%
object with const bool property..........................+26.99%
object with const object property........................+10.85%
object with const null property..........................+29.47%
Back to patch-1 b4e5606

@cesco69 cesco69 changed the title perf(serializer): remove all anonymous functions perf(serializer): remove all anonymous functions (huge perf improvements!) Jan 15, 2026
@cesco69 cesco69 marked this pull request as ready for review January 15, 2026 11:03
@cesco69
Copy link
Contributor Author

cesco69 commented Jan 15, 2026

If this PR gets approved I would like to move the methods asString, asNumber, asBoolean, asDateTime, asDate, asTime, asUnsafeString inline as well ( see #813 ), eg.:

function inlineAsInteger (options, input) {
  let roundingFn = 'Math.trunc'
  if (options && options.rounding) {
    switch (options.rounding) {
      case 'floor':
        roundingFn = 'Math.floor'
        break
      case 'ceil':
        roundingFn = 'Math.ceil'
        break
      case 'round':
        roundingFn = 'Math.round'
        break
    }
  }

  return `
    if (Number.isInteger(${input})) {
      json += ${input}
    } else if (typeof ${input} === 'bigint') {
      json += ${input}.toString()
    } else {
      const integer = ${roundingFn}(${input})
      if (integer === Infinity || integer === -Infinity || integer !== integer) {
        throw new Error('The value "' + ${input} + '" cannot be converted to an integer.')
      }
      json += integer
    }
  `
}

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

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

wow, lgtm

@cesco69 cesco69 mentioned this pull request Jan 15, 2026
@mcollina mcollina merged commit 781be6a into fastify:main Jan 15, 2026
14 checks passed
@gurgunday
Copy link
Member

Wow, good work!

@Eomm
Copy link
Member

Eomm commented Jan 17, 2026

Should we update the readme bench too?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants