GeezerVison is showing github.com — Hyperlink: correctness, runtime and bundle work from the 2.3.0 review · Issue #769 · docs-plus/docs.plus
Hyperlink: correctness, runtime and bundle work from the 2.3.0 review #769DescriptionTracking issue for the 2026-11-12 review of @docs.plus/extension-hyperlink 2.3.0: bundle size, runtime cost and correctness.
Summary
Bundlephobia shows 91,188 B min / 35,890 B gzip. That number counts linkifyjs and Floating UI as new code. A StarterKit app really pays 70,854 / 25,213 B, and the docs.plus webapp pays 54,947 / 19,144 B, because both already ship most of the dependencies. The review found one live bug that every pad and chat message can hit, two more bugs, and four measured optimizations. It also rejected several ideas with measured reasons (below).
Order and status
| # |
Issue |
Kind |
Depends on |
Ruling |
Status |
| 1 |
#770 Keep the whole link when part of it is formatted |
bug |
— |
— |
committed c9cd2c98a (not pushed); chat message box check owed (needs sign-in); production check |
| 2 |
#772 Keep a custom href when autolink runs |
bug |
#770 |
— |
committed c9cd2c98a (not pushed); Mod-k form flow and production check owed |
| 3 |
#773 Faster autolink on big pastes and select-all formatting |
perf |
#770 |
— |
committed c9cd2c98a (not pushed); jsdom −51 % paste, −33 % Bold; foreground browser run, typing profile and production check owed |
| 4 |
#776 Store the special-URL catalog as typed rows |
perf |
— |
— |
committed c9cd2c98a (not pushed); −96 B gzip; production glance owed |
| 5 |
#771 A peer brings back a removed link |
bug |
#770 |
9 |
waits on the ruling |
| 6 |
#774 PURE build and its preflight guard |
build |
— |
4 |
waits on the ruling |
| 7 |
#775 HyperlinkBase without the prebuilt popovers |
feat |
#774 |
— |
waits on #774 |
| 8 |
#777 Load link-icon images only near the screen |
perf |
— |
— |
measure first |
| 9 |
#778 Enter inside a link |
decision |
#770 |
2 |
waits on the ruling |
Findings from the work (not filed)
Rulings the maintainer owes
| Ruling |
Question |
Recommendation |
Issue |
| 2 |
Enter inside a link: keep today, match upstream, or unlink both halves? |
Keep today |
#778 |
| 4 |
How the PURE build reaches two packages under the single shared tsup config |
One opt-in option in defineTiptapExtensionConfig |
#774 |
| 9 |
Should a client autolink text that a peer or the server wrote? |
No: skip remote non-undo transactions; link server writes on the server if wanted |
#771 |
Rules for every child issue
- Bench before and after.
extensions/extension-hyperlink/scripts/bench/ (added with Hyperlink: keep the whole link when part of it is formatted #770) compares the working tree with a git ref, plus a control copy that shows the noise. Run NODE_ENV=production bun scripts/bench/run.ts --ref <commit before the change> from the package, and scripts/bench/browser.ts for a real browser. Post the before as a Measured comment before the first commit, and the after on the commit.
- UX first. A saving that changes what a user sees or does is a regression. Each child lists its UX contract: what must not change, and the check that proves it. Bug fixes compare against upstream
@tiptap/extension-link 3.32.2, not against today's buggy code.
- Prove tests by sabotage. Each new test fails with its fix reverted.
- Real browser for speed claims. jsdom numbers compare variants only. Quote a speed number only from a foreground browser run, with its condition.
- Gates.
bun run test:unit, bun run typecheck, lint and Prettier on changed files, and the full package Cypress suite (env -u ELECTRON_RUN_AS_NODE bun run test:e2e).
- Owner. Agent in the repo: code, tests, bench, CHANGELOG. Maintainer: rulings, commits review, release, production checks.
Not doing (measured reasons)
- Replace linkifyjs: 0 bytes for StarterKit apps, which already ship it through
@tiptap/extension-link.
- Lazy
import() of the hovercard engine through editor.registerPlugin: it rebuilds every plugin view and forces a full-doc re-render. A lazy built-in card only (plugin stays eager) is not measured; the webapp always draws its own card, so it gains nothing.
- Lazy prebuilt popovers or openers:
HyperlinkBase (Hyperlink: add HyperlinkBase without the prebuilt popovers #775) gives the webapp these bytes; CJS gains nothing; iOS keyboard focus inside the user gesture is a risk.
- Subpath entries: 665 B more than the PURE build for helpers; in CJS each entry would get its own popover controller and tooltip bubble.
- Move
isSafeHref into a module without linkifyjs: costs every StarterKit app +52 / +125 B; the PURE build already gives helper-only users about 19 kB.
- Feature factories for hovercard, pasteTitle and linkIcon: a major version; −5.9 kB gzip only for apps that use none of the three; the webapp gains 0. Revisit at the product 3.0 major.
- Keystroke fast path: about 16 µs of a 95 µs key in a real browser, +199 B gzip, and it skips work next to links.
- Remove the built-in hovercard card: ruled to stay (
extensions/extension-hyperlink/AGENTS.md).
- Drop sourcemaps: 555,127 B of install size, 0 B in an app bundle.
- Trim Floating UI imports or minify the package CSS: every import is used; app pipelines minify CSS.
Settled rules this work keeps
@docs.plus/floating-popover and @docs.plus/floating-tooltip stay bundled; @floating-ui/dom stays external; CJS stays; linkifyjs stays; the hyperlink mark name and the Markdown hooks stay.
ActivityHMarzban commented on Nov 12, 2026 CollaboratorAuthor More actions Measured: commit c9cd2c98a against its parent
The commit holds #770, #772, #773 and #776 together. Package bench from extensions/extension-hyperlink: NODE_ENV=production bun scripts/bench/run.ts --ref c9cd2c98a~1.
| Metric |
base |
head |
Change |
Bytes: npm dist/index.js (min / gz) |
57,127 / 20,500 |
57,013 / 20,861 |
-114 / +361 |
| Bytes: a StarterKit app pays (min / gz) |
70,854 / 25,213 |
70,776 / 25,582 |
-78 / +369 |
Bytes: normalizeHref-only import (min / gz) |
368,030 / 121,399 |
367,952 / 121,722 |
-78 / +323 |
| Behaviour: all cases |
5/18 |
18/18 |
+13 |
| Behaviour: strip |
0/10 |
10/10 |
|
| Behaviour: undo |
1/2 |
2/2 |
|
| Behaviour: keep |
4/4 |
4/4 |
|
| Behaviour: href |
0/2 |
2/2 |
|
| Speed: pasteText2000 (ms) |
479.661 |
228.795 |
-52% · control -1% |
| Speed: boldAll2000 (ms) |
350.266 |
227.547 |
-35% · control +3% |
| Speed: typeEnd2000 (µs/key) |
152.222 |
153.656 |
+1% · control +0% |
| Speed: pasteOverSelection250k (ms) |
22.605 |
12.216 |
-46% · control +3% |
| Speed: boldPartOfLink (ms) |
0.159 |
0.126 |
-21% · control -1% · output differs |
| Speed: mount1000Links (ms) |
26.997 |
27.02 |
+0% · control -2% |
jsdom on Bun 1.4.4, darwin arm64, 9 rotated rounds, 2026-11-12. output differs on boldPartOfLink is the #770 fix itself. A StarterKit app pays −78 B minified but +369 B gzip: #776 saves minified bytes, and the #773 pass adds gzip bytes. No browser number here: a foreground run is still owed on #773. HMarzban commented on Nov 12, 2026 CollaboratorAuthor More actions Workflow results, wf3 and wf4 (2026-11-12, local, read-only)
Measured on c9cd2c98a against c9cd2c98a~1. Machine: Apple M5 Max, Bun 1.4.4, headless Chrome for Testing 158.0.8127.18. That is build 1254, because Playwright 1.64.1's build 1248 is not installed. Pad checks ran on the local webapp at :3001, a next dev build. A verifier re-ran each number. Status per issue:
Finding: chunked input links only the last word. When one transaction inserts several words, autolink checks only the last word. On fresh local pads, a one-chunk insertText of see example.com now left example.com unlinked in 0 of 10 runs. A CDP IME phrase commit also linked 0 of 10. Key-by-key typing linked 10 of 10. With the URL as the last word (go to example.com ), both chunk paths linked 5 of 5. A verifier re-run on 5 new pads matched. Key-typing one more word (more ) did not repair the miss (0 of 20). Enter, blur and reload were not tested. Upstream Link 3.32.2 has the same rule, so this is not a regression. A scratch prototype fixed it for +234 B gzip, but only on a scratch page, not on a pad. It should become a low-priority issue after ruling 9. Still owed: one real macOS dictation check.
Raw data: local workflow folders wf3/ and wf4/, with one subfolder per task. Verifier re-runs are in the v-*, verify-* and p1-verify subfolders.
Pad profile, final: on a 2,025-block pad, select-all Bold takes about 4.6–4.7 s (n = 7 per pad, A/A −1.13 %). Autolink is 1.5 % of it. The CPU profile puts 32–35 % in y-prosemirror / y-tiptap / Yjs and 30–34 % in prosemirror-transform; the hyperlink extension is 0.38 %. Details on #773.
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|