vuejs/vue-vapor

[Optimization] Cache prev values in closure instead of `recordPropMetadata`

yyx990803 opened this issue · 1 comments

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
  }
]
<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 setAttr3(el, key, value) {
        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
      }
    }

    function setClass3(el, value) {
      if ((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 updateThree = (() => {
      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 => {
        cls !== val && setClass3(n1,(cls = val))
        id !== val && setAttr3(n1, 'id',(id = val))
        cls2 !== val && setClass3(n2,(cls2 = val))
        id2 !== val && setAttr3(n3, 'id', (id2 = val))
        cls3 !== val && setClass3(n4, (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')
      })
      .add('in closure 2', () => {
        updateThree(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)": "59.88 ± 6.20%",
    "Latency median (ns)": "0.00",
    "Throughput average (ops/s)": "16690096 ± 0.00%",
    "Throughput median (ops/s)": "16700090",
    "Samples": 1670009
  },
  {
    "Task name": "in metadata",
    "Latency average (ns)": "84.41 ± 6.21%",
    "Latency median (ns)": "0.00",
    "Throughput average (ops/s)": "11836568 ± 0.01%",
    "Throughput median (ops/s)": "11846530",
    "Samples": 1184653
  },
  {
    "Task name": "in closure 2",
    "Latency average (ns)": "56.18 ± 6.20%",
    "Latency median (ns)": "0.00",
    "Throughput average (ops/s)": "17788545 ± 0.00%",
    "Throughput median (ops/s)": "17798540",
    "Samples": 1779854
  }
]