Embed snippets preload the Mapbox engine, so a partner page fetches it alongside the bundle

The card asked for Vue to be bundled inline to make maps faster. It
already is: vite.config.js externalises only jspdf, and every IIFE
carries its Vue runtime. The measurement (internal/measure-map-load.mjs,
cold, 10 Mbps / 60 ms, served built tree) found one serial runtime
fetch before first paint, on partner pages only. A page that pastes the
embed snippet has no leaf page to fetch mapbox-gl.js (~480 KB gz), so the
bundle requested it after it had downloaded, run and mounted. Leaf pages
already load it in parallel.

Map profiles' script snippets now start with a preload for the engine,
at the URL read from useMapInstance.js ENGINES.mapbox.js so the two can
never differ. It has no crossorigin attribute because
ensureEngineScript injects a plain <script>, and a mismatched preload is
not reused. Non-map profiles get no preload. A comment tells a partner
who switches to provider "maplibre" to delete the line.

Partner embed, before -> after (2 cold runs):
  doxa-simple-map  engine start 1.06 -> 0.20 s, done 2.26 -> 0.95 s,
                   style start 2.73 -> 1.94 s
  regions-mfe      engine start 2.67 -> 0.11 s, done 3.25 -> 1.13 s
                   (style start unchanged: the composite mounts its map late)
  gospel resources engine start 0.81 -> 0.27 s
One engine request per run: the preload is reused, never a double
download. The time until tiles are loaded is dominated by tiles, data
and drawing, and moved within noise.

Held, per dx-shepherd: no shared Vue, no inlined mapbox-gl, and lighter
basemaps are for the Driver to decide.
This commit is contained in:
doxa-maps-88655c70 2026-09-25 08:56:05 +00:00
parent 11a4d06935
commit 4a6886d2c9
2 changed files with 31 additions and 2 deletions

View file

@ -161,6 +161,19 @@ const DEFINE_RE = /customElements\.define\(\s*['"]([^'"]+)['"]/g // registered
// manifest lists exactly what the dev manifest lists (a blank template-map-a/b in the built studio was // manifest lists exactly what the dev manifest lists (a blank template-map-a/b in the built studio was
// the only difference, found 2026-09-25). Not gatedProfiles: that list is governance (private data). // the only difference, found 2026-09-25). Not gatedProfiles: that list is governance (private data).
const SCAFFOLD_BUNDLES = ['template-bundle'] const SCAFFOLD_BUNDLES = ['template-bundle']
// ★ THE MAPBOX ENGINE URL, read from the ONE place that loads it (useMapInstance.js ENGINES.mapbox.js),
// so the embed snippet's preload can never point at a different version than the one the bundle
// requests — a mismatched preload is not used, and the engine would download twice.
const MAPBOX_GL_JS = (() => {
try {
const src = readFileSync(resolve(__dirname, '..', 'library', 'composables', 'useMapInstance.js'), 'utf8')
const m = src.match(/mapbox:\s*\{[^}]*?js:\s*'([^']+mapbox-gl\.js)'/)
if (m) return m[1]
} catch { /* fall through */ }
console.warn('[generate-tree] could not read the Mapbox engine URL from useMapInstance.js; embed snippets get no preload')
return ''
})()
const isBundleDir = (name) => !name.startsWith('_') && !name.startsWith('.') && !DEST_CONFIG.gatedProfiles.includes(name) && !SCAFFOLD_BUNDLES.includes(name) const isBundleDir = (name) => !name.startsWith('_') && !name.startsWith('.') && !DEST_CONFIG.gatedProfiles.includes(name) && !SCAFFOLD_BUNDLES.includes(name)
// ★ CUSTOM-ELEMENT NAME RULE (HTML spec): lowercase, must contain a hyphen, no spaces. // ★ CUSTOM-ELEMENT NAME RULE (HTML spec): lowercase, must contain a hyphen, no spaces.
@ -1204,7 +1217,20 @@ function scanBundles() {
isMap, isMap,
iframeUrl, iframeUrl,
scriptUrl, scriptUrl,
embed: { iframe: iframeEmbed, script: scriptEmbed }, // ★ ENGINE PRELOAD, map snippets only (measured 2026-09-25, cold, 10 Mbps). A partner page that
// pastes this snippet has no leaf page to fetch mapbox-gl.js, so the bundle requested it only
// after it had downloaded, run and mounted: doxa-simple-map's engine started at 1.06 s, right
// after its bundle (0.99 s); regions-mfe's at 2.67 s. A preload lets the ~480 KB engine download
// alongside the bundle. NO crossorigin attribute, on purpose: ensureEngineScript injects a plain
// <script src>, and a preload is only reused when its request mode matches — with crossorigin it
// would download twice. Mapbox only: a non-map profile gets none, and a partner switching to
// provider:"maplibre" is told to drop the line, since that engine must make zero Mapbox requests.
embed: {
iframe: iframeEmbed,
script: isMap && MAPBOX_GL_JS
? `<!-- Mapbox engine, fetched early; remove this line if you set "provider":"maplibre" -->\n<link rel="preload" as="script" href="${MAPBOX_GL_JS}">\n` + scriptEmbed
: scriptEmbed,
},
...(instances.length ? { instances } : {}), ...(instances.length ? { instances } : {}),
// Extra profile-config keys from <profile>.meta.json `config` (e.g. pinHint:false). // Extra profile-config keys from <profile>.meta.json `config` (e.g. pinHint:false).
// GENERATOR-INTERNAL: stripped before the manifest is written (see writeManifestAndHtml) // GENERATOR-INTERNAL: stripped before the manifest is written (see writeManifestAndHtml)

View file

@ -87,7 +87,10 @@ for (const ref of ONLY) {
const first = (k) => all.filter((x) => x.kind === k).sort((a, c) => a.start - c.start)[0] const first = (k) => all.filter((x) => x.kind === k).sort((a, c) => a.start - c.start)[0]
const bundleReq = all.find((x) => x.kind === 'bundle' && x.url.includes(`/${bundle}/${bundle}.js`)) || first('bundle') const bundleReq = all.find((x) => x.kind === 'bundle' && x.url.includes(`/${bundle}/${bundle}.js`)) || first('bundle')
const eng = first('engine'), sty = first('style'), dat = first('data') const eng = first('engine'), sty = first('style'), dat = first('data')
// A preload the browser cannot reuse (request mode mismatch) shows up as a SECOND engine request.
const engineRequests = all.filter((x) => x.kind === 'engine').length
runs.push({ runs.push({
engineRequests,
loaded, styled, bundleEnd: bundleReq && bundleReq.end, engineStart: eng && eng.start, engineEnd: eng && eng.end, loaded, styled, bundleEnd: bundleReq && bundleReq.end, engineStart: eng && eng.start, engineEnd: eng && eng.end,
styleStart: sty && sty.start, dataStart: dat && dat.start, styleStart: sty && sty.start, dataStart: dat && dat.start,
bytes: { bundle: 0 }, bytes: { bundle: 0 },
@ -95,7 +98,7 @@ for (const ref of ONLY) {
await ctx.close() await ctx.close()
} }
const med = (k) => { const v = runs.map((x) => x[k]).filter((x) => x != null).sort((a, c) => a - c); return v.length ? v[Math.floor(v.length / 2)] : null } const med = (k) => { const v = runs.map((x) => x[k]).filter((x) => x != null).sort((a, c) => a - c); return v.length ? v[Math.floor(v.length / 2)] : null }
console.log(' measured ' + ref + ': ' + JSON.stringify(runs.map((x) => x.loaded))) console.log(' measured ' + ref + ': ' + JSON.stringify(runs.map((x) => x.loaded)) + ' · engine requests per run ' + JSON.stringify(runs.map((x) => x.engineRequests)))
rows.push({ ref, styled: med('styled'), loaded: med('loaded'), bundleEnd: med('bundleEnd'), engineStart: med('engineStart'), engineEnd: med('engineEnd'), styleStart: med('styleStart'), dataStart: med('dataStart'), runs: runs.map((x) => x.loaded) }) rows.push({ ref, styled: med('styled'), loaded: med('loaded'), bundleEnd: med('bundleEnd'), engineStart: med('engineStart'), engineEnd: med('engineEnd'), styleStart: med('styleStart'), dataStart: med('dataStart'), runs: runs.map((x) => x.loaded) })
} }
await browser.close(); srv.kill(); host.close() await browser.close(); srv.kill(); host.close()