Skip to content

Fast font open path and native dev build cleanup #108

Description

@kostyafarber

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions