Skip to content

[Optimization] Cache prev values in closure instead of recordPropMetadata #297

Open
@yyx990803

Description

Currently prop-setting helpers do this:

function setClass(el, value) {
  const prev = recordPropMetadata(el, 'class', value)
  if (value !== prev && (value || prev)) {
    el.className = value
  }
}

function recordPropMetadata(el, key, value) {
  const metadata = getMetadata(el)[0]
  const prev = metadata[key]
  if (prev !== value) metadata[key] = value
  return prev
}

function getMetadata(el) {
  return el.$$metadata || (el.$$metadata = [{}, {}])
}

recordPropMetadata has noticeable overhead on every call:

  • extra index / key access
  • extra array / object allocation

Changes Needed

For template

<div>
   <div :id="foo" :class="bar"></div>
</div>

Current codegen:

const n1 = /* ... */
_renderEffect(() => _setDOMProp(n1, "id", foo))
_renderEffect(() => _setClass(n1, bar))

Should be changed to (storing prev update values in the local closure):

const n1 = /* ... */
let _id, _cls
_renderEffect(() => _setDOMProp(n1, "id", _id, (_id = foo)))
_renderEffect(() => _setClass(n1, _cls, (_cls = bar)))

And in the relevant helpers, prev value should come from the argument instead of element metadata.

Benchmark

A simple benchmark that simulate a typical vapor update function:

<script type="module">
  import { Bench } from 'https://esm.sh/tinybench'

  function recordPropMetadata(el, key, value) {
    const metadata = getMetadata(el)[0]
    const prev = metadata[key]
    if (prev !== value) metadata[key] = value
    return prev
  }

  function getMetadata(el) {
    return el.$$metadata || (el.$$metadata = [{}, {}])
  }

  function setAttr(el, key, value) {
    const oldVal = recordPropMetadata(el, key, value)
    if (value !== oldVal) {
      if (value != null) {
        el.setAttribute(key, value)
      } else {
        el.removeAttribute(key)
      }
    }
  }

  function setAttr2(el, key, oldVal, value) {
    if (value !== oldVal) {
      if (value != null) {
        el.setAttribute(key, value)
      } else {
        el.removeAttribute(key)
      }
    }
  }

  function setClass(el, value) {
    const prev = recordPropMetadata(el, 'class', value)
    if (value !== prev && (value || prev)) {
      el.className = value
    }
  }

  function setClass2(el, prev, value) {
    if (value !== prev && (value || prev)) {
      el.className = value
    }
  }

  const updateOne = (() => {
    const n1 = document.createElement('div')
    const n2 = document.createElement('div')
    const n3 = document.createElement('div')
    const n4 = document.createElement('div')
    return val => {
      setClass(n1, val)
      setAttr(n1, 'id', val)
      setClass(n2, val)
      setAttr(n3, 'id', val)
      setClass(n4, val)
    }
  })()

  const updateTwo = (() => {
    const n1 = document.createElement('div')
    const n2 = document.createElement('div')
    const n3 = document.createElement('div')
    const n4 = document.createElement('div')
    let cls, id, cls2, id2, cls3
    return val => {
      setClass2(n1, cls, (cls = val))
      setAttr2(n1, 'id', id, (id = val))
      setClass2(n2, val, (cls2 = val))
      setAttr2(n3, 'id', val, (id2 = val))
      setClass2(n4, val, (cls3 = val))
    }
  })()

  const bench = new Bench({ name: 'old value handling for set ops', time: 100 })
  let i
  bench
    .add('in closure', () => {
      updateTwo(i++ % 5 ? 'foo' : 'bar')
    })
    .add('in metadata', () => {
      updateOne(i++ % 5 ? 'foo' : 'bar')
    })

  await bench.run()

  console.log(bench.name)
  const output = JSON.stringify(bench.table(), null, 2)
  document.getElementById('output').textContent = output
</script>

<pre id="output"></pre>

Result in Chrome:

[
  {
    "Task name": "in closure",
    "Latency average (ns)": "51.16 ± 6.19%",
    "Latency median (ns)": "0.00",
    "Throughput average (ops/s)": "19537649 ± 0.00%",
    "Throughput median (ops/s)": "19547644",
    "Samples": 1956719
  },
  {
    "Task name": "in metadata",
    "Latency average (ns)": "79.90 ± 6.20%",
    "Latency median (ns)": "0.00",
    "Throughput average (ops/s)": "12505888 ± 0.00%",
    "Throughput median (ops/s)": "12515880",
    "Samples": 1251588
  }
]

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions