Context
Opening a real .glyphspackage from the landing page still feels slower than expected, even after moving native dev builds to release mode. This issue captures the findings from the open-path investigation and the follow-up work needed to make font opening feel native-fast without adding preservation hacks or sync cache behavior.
Findings
pnpm run dev should build the native bridge in release mode. Debug native builds made the font import path look much worse than it was.
- Stale package-local
node_modules directories can shadow the root pnpm install and break scripts such as napi and electron-forge. clean should remove package-local node_modules in addition to .vite, out, root node_modules, .turbo, and cargo clean.
- The large NAPI boundary call for the glyph list is not the current bottleneck.
getGlyphs measured around 3ms for ~985 glyph records.
- Eager store materialization is expensive for imported fonts.
replace_font_state measured around 180ms and should not be on the initial import/open path unless needed.
- Source package recovery/preservation logic must not run for non-
.shift imports. Avoid any "preserve unknown fields by accident" hacks in fast import paths.
- Electron
BrowserWindow.maximize() shows a hidden window, so it is not a good way to prepare a document window invisibly. A better PoC was creating the document window hidden with final bounds, loading it, then revealing after the renderer has painted useful content.
- The native file dialog dominates some total
file.open timings, so measure from after file selection separately from the whole command duration.
- Upstream
googlefonts/fontc has moved on from our pinned versions (fontc = 0.2.0, glyphs-reader = 0.2.0). Current upstream has newer crates, but glyphs-reader still eagerly reads every .glyph file in a .glyphspackage package before returning a Font.
daltonmaag/glyphs-plist shares ancestry with the same Raph Levien plist parser family. It may be useful as a lower-level parser building block, but it does not look like a drop-in answer for fast .glyphspackage opening.
- The glyph grid renders preview SVGs, so a pure metadata-only index is not enough for final first paint. We need a staged preview hydration batch for visible glyphs.
Measured Shape
Representative release-build timings from the investigation:
GlyphsFont::load: roughly 130-240ms on the tested Shantell Sans .glyphspackage.
- Shift metadata/source/glyph-record conversion with geometry skipped: roughly
10-12ms.
- Feature/kerning conversion: roughly
2ms.
- Workspace bridge open with store materialization removed: roughly
250-450ms depending on run.
- Eager store replacement when enabled: roughly
180ms extra.
- Renderer
workspace.open and document open handler were mostly waiting on the workspace open call once window maximize was removed.
- Old renderer-triggered
window.maximise command could block the main process for roughly 440ms.
Proposed Direction
Implement a staged document open model instead of eagerly loading the entire font into the editable workspace state.
-
Add a format-neutral document index API:
workspace.openIndex(path) -> DocumentIndex
- includes family/style, axes/sources, metrics, glyph order, glyph names, unicodes, export state, and cheap widths where available.
-
Add batched glyph hydration APIs:
workspace.hydrateGlyphPreviews(documentId, glyphIds, sourceId)
workspace.hydrateGlyphForEdit(documentId, glyphId, options)
- batch visible glyphs to avoid one NAPI call per glyph.
-
For .glyphspackage:
- read
fontinfo.plist and order.plist immediately;
- shallow-scan glyph files for index fields;
- hydrate only visible grid previews first;
- hydrate component dependencies in the same batch;
- fully hydrate selected/opened glyphs and source layers only when entering edit workflows.
-
For UFO/designspace:
- UFO: read root metadata plus
glyphs/contents.plist, then shallow/hydrate .glif files as needed.
- Designspace: read the
.designspace XML for axes/sources/default source, then index source UFOs without loading all full geometry upfront.
-
Keep full readers as correctness backstops:
glyphs-reader, norad, and designspace readers remain authoritative for full-fidelity import/export paths.
- the fast path is an explicit partial-read/open model, not a preservation layer.
Native Dev Build Cleanup
pnpm run dev should use the release native bridge by default.
- Keep a separate debug command for bridge debugging, e.g.
dev:native:debug.
- Root/package scripts should not depend on janky relative invocations of
../../node_modules/.bin/....
pnpm clean should remove package-local stale installs so root pnpm resolution stays predictable.
UI/Open Path Cleanup
- Keep the landing/launcher window small.
- Open loaded fonts in a document-sized window.
- Avoid maximizing a visible launcher window and swapping its content.
- Avoid showing a blank full-size document window behind the landing page.
- Prefer hidden document-window creation with final bounds and renderer-controlled reveal after first meaningful paint.
- Continue improving error surfacing on the landing load path. Current generic errors like
workspace file-system error are not actionable enough.
Related Regressions/Checks
- Metrics were reported as not rendering during this investigation; verify once the open path changes settle.
- Pen-tool point creation was reported as not adding points; verify separately if not already fixed.
- Keep an eye on route-change grid flashing. Some of it may be avoided by separating launcher/document windows and by not remounting the grid unnecessarily.
Acceptance Criteria
- Release-native
pnpm run dev works from a clean checkout after pnpm install.
- Opening a
.glyphspackage after file selection shows a document window with useful grid content without a full-screen blank flash.
- Initial open does not eagerly materialize the full store for imported fonts.
- Initial open does not eagerly parse all full glyph geometry when only the grid is needed.
- Visible glyph preview hydration is batched.
- No sync caches or preservation hacks are introduced.
- Error messages from the landing load path identify the failing operation and underlying cause.
Context
Opening a real
.glyphspackagefrom the landing page still feels slower than expected, even after moving native dev builds to release mode. This issue captures the findings from the open-path investigation and the follow-up work needed to make font opening feel native-fast without adding preservation hacks or sync cache behavior.Findings
pnpm run devshould build the native bridge in release mode. Debug native builds made the font import path look much worse than it was.node_modulesdirectories can shadow the root pnpm install and break scripts such asnapiandelectron-forge.cleanshould remove package-localnode_modulesin addition to.vite,out, rootnode_modules,.turbo, andcargo clean.getGlyphsmeasured around3msfor ~985 glyph records.replace_font_statemeasured around180msand should not be on the initial import/open path unless needed..shiftimports. Avoid any "preserve unknown fields by accident" hacks in fast import paths.BrowserWindow.maximize()shows a hidden window, so it is not a good way to prepare a document window invisibly. A better PoC was creating the document window hidden with final bounds, loading it, then revealing after the renderer has painted useful content.file.opentimings, so measure from after file selection separately from the whole command duration.googlefonts/fontchas moved on from our pinned versions (fontc = 0.2.0,glyphs-reader = 0.2.0). Current upstream has newer crates, butglyphs-readerstill eagerly reads every.glyphfile in a.glyphspackagepackage before returning aFont.daltonmaag/glyphs-plistshares ancestry with the same Raph Levien plist parser family. It may be useful as a lower-level parser building block, but it does not look like a drop-in answer for fast.glyphspackageopening.Measured Shape
Representative release-build timings from the investigation:
GlyphsFont::load: roughly130-240mson the tested Shantell Sans.glyphspackage.10-12ms.2ms.250-450msdepending on run.180msextra.workspace.openand document open handler were mostly waiting on the workspace open call once window maximize was removed.window.maximisecommand could block the main process for roughly440ms.Proposed Direction
Implement a staged document open model instead of eagerly loading the entire font into the editable workspace state.
Add a format-neutral document index API:
workspace.openIndex(path) -> DocumentIndexAdd batched glyph hydration APIs:
workspace.hydrateGlyphPreviews(documentId, glyphIds, sourceId)workspace.hydrateGlyphForEdit(documentId, glyphId, options)For
.glyphspackage:fontinfo.plistandorder.plistimmediately;For UFO/designspace:
glyphs/contents.plist, then shallow/hydrate.gliffiles as needed..designspaceXML for axes/sources/default source, then index source UFOs without loading all full geometry upfront.Keep full readers as correctness backstops:
glyphs-reader,norad, and designspace readers remain authoritative for full-fidelity import/export paths.Native Dev Build Cleanup
pnpm run devshould use the release native bridge by default.dev:native:debug.../../node_modules/.bin/....pnpm cleanshould remove package-local stale installs so root pnpm resolution stays predictable.UI/Open Path Cleanup
workspace file-system errorare not actionable enough.Related Regressions/Checks
Acceptance Criteria
pnpm run devworks from a clean checkout afterpnpm install..glyphspackageafter file selection shows a document window with useful grid content without a full-screen blank flash.