Skip to content

perf: lazy frontMatter and TOC serialization in QuickJS payload fast path #1187

Description

@zeroedin

Description

setPayloadFrontMatter calls jsonCodec.Marshal(fm) + ParseJSON() on every page for every hook call, even though most post-render hooks never read page.frontMatter. On the RHDS benchmark (820 pages), this costs 3.2 seconds of JSON work on ~1KB maps that are never accessed.

Same pattern applies to TOC in callHookTransformPayloadjsonCodec.Marshal(v.TOC) + ParseJSON() per page, rarely read by plugins.

Fix

Use Object.defineProperty with a getter backed by a Go callback. The JSON marshal + ParseJSON only runs if the plugin actually accesses the property.

frontMatter

Store the Go map in r.pendingFM. Install a lazy getter via a pre-compiled __installLazyFM function:

function __installLazyFM(target) {
  Object.defineProperty(target, 'frontMatter', {
    get: function() {
      var v = JSON.parse(__resolveFM());  // Go callback, marshals on demand
      Object.defineProperty(this, 'frontMatter', {value: v, writable: true, configurable: true});
      return v;
    },
    set: function(v) {
      Object.defineProperty(this, 'frontMatter', {value: v, writable: true, configurable: true});
    },
    configurable: true
  });
}

The setter is required — plugins that write page.frontMatter = {...} before reading must not throw TypeError. The setter replaces the accessor with a writable data property.

__resolveFM is a Go callback registered via ctx.SetFunc during Init(). It reads r.pendingFM, marshals to JSON, and returns the string. Same pattern for __resolveTOC / r.pendingTOC.

TOC

Identical pattern with __installLazyTOC and __resolveTOC. Only installed when len(v.TOC) > 0.

Measured impact

RHDS benchmark: frontMatter serialization dropped from 3.2s to 36ms (89x). Total build dropped from 20.2s to 15.9s. Combined with other optimizations, total reached 14.8s (faster than v0.5.0's 15.8s).

Spec test needs

  • Plugin reads page.frontMatter.title → receives correct value (getter fires, JSON resolves)
  • Plugin writes page.frontMatter = {custom: true} before reading → no TypeError (setter works)
  • Plugin never reads frontMatter → no error, no perf penalty (getter never fires)
  • Plugin reads frontMatter twice → second access returns cached value (getter self-replaces with data property)
  • Nil frontMatter → page.frontMatter is empty object (not undefined, not null)
  • Empty frontMatter map → same as nil
  • Same patterns for TOC: read, write-before-read, never-read, nil, empty

Refs #1185, #1180

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions