What this post covers
While refining a hybrid block editor, four layers overlapped: why the production bundle is similarly heavy across all pages, text selection/deletion spanning multiple blocks, the selection toolbar drifting after scroll, and UI that reads the whole body (like a table of contents) re-rendering on every keystroke. This organizes, per task, what situation each work faced, what it aimed for, how it was solved, and what was learned.
Day summary
| Task | What I wanted | What I did | Result |
|---|---|---|---|
| Bundle | Split heavy chunks per route — keep shared vendor out of all pages | remove chunks:"all" + fixed name from splitChunks catch-all, adjust chunks:"initial"·maxSize, static→dynamic import | heavy editor/chart libraries dropped from the first page |
| Cross-block | Derive the range from Selection at operation time, not in state | single-block via guest, cross-block only via document handler — gather boundary split in one place | delete·mark reuse the boundary split, safe without an owning PM |
| Floating UI | Re-measure the anchor on every scroll·resize while open — prevent stuck after DnD | trigger UI via selectionchange, update coordinates on scroll (capture) + resize | toolbar doesn't drift from the selection after scroll·resize |
| Context | Separate the frequently-changing document tree from broad runtime Context | narrow the subscription scope with a dedicated Context for TOC·derived UI | blocked TOC·stats re-rendering on single-character typing |
1. Webpack shared-vendor trap
Background (concepts)
- Code splitting: loading only the JS each route needs, split apart. With
dynamic importyou can load heavy libraries lazily.
What the situation was
After a production build, the First Load JS of several routes is similarly large. Even with code splitting done, heavy editor/chart libraries get pulled onto the first page together.
Core work
If the splitChunks catch-all vendor has chunks: "all" and a fixed name, code splitting can be neutralized. Along with chunks: "initial"·maxSize·removing the fixed name, you must change the static import chain to dynamic import for the heavy libraries to drop. As a result, heavy editor/chart libraries dropped from the first page.
Takeaways
- If all routes are heavy together, suspect the shared vendor chunk first.
chunks: "all"+ a fixed name can neutralize dynamic import.
→ Webpack splitChunks vendor shared-chunk trap
2. Cross-block text operations
Background (concepts)
- Guest editor: a pattern where only one focused block is edited as rich text. (see focused-guest-editor)
What the situation was
Since each block has its own guest editor, a drag selection from the end of one block to the start of the next exists only as a native Selection. There is no "which PM is the owner."
Core work
Instead of putting the range in state, derive it from Selection at operation time, and gather the block-boundary split in one place so delete·mark reuse it. Single-block goes via the guest, cross-block only via the document handler. As a result, delete·mark reused the boundary split and worked safely without an owning PM.
Takeaways
- Don't put a cross-block range in React state — derive it from the Selection API, and gather block-boundary handling in one place.
→ Cross-block text range operations
3. Floating-UI coordinates
Background (concepts)
- Floating UI: a toolbar/popover that floats near the selection with
position: fixed.
What the situation was
If you capture the toolbar coordinates only once after selecting text, the toolbar drifts from the selection after scroll·resize.
Core work
A one-time position: fixed coordinate capture isn't enough. While the UI is open, re-measure the anchor on scroll capture and resize. For the selection-UI trigger, selectionchange avoids stuck-after-DnD more easily than mousedown/mouseup. As a result, the toolbar didn't drift from the selection even after scroll·resize.
Takeaways
- A one-time fixed coordinate capture isn't enough. When the scroll container moves, re-measure the anchor.
selectionchangeis often more advantageous thanmousedown/mouseupfor selection UI after DnD.
→ Fixed-position floating UI and scroll tracking · selectionchange for floating selection UI
4. Context subscription separation
Background (concepts)
- Derived UI: UI built by reading the whole body tree, like a TOC or stats. It easily re-renders whenever the document changes by a single character.
What the situation was
Whenever the body tree changes by a single character, the TOC·stats re-render along with it.
Core work
Derived UI that reads the whole body, like a TOC, doesn't put the frequently-changing document into a broad runtime Context but separates it into a dedicated Context to preserve memo·render count. As a result, the TOC·stats re-rendering on single-character typing was blocked.
Takeaways
- Derived UI that reads the whole body doesn't put the document tree in a broad Context.
→ React Context render granularity