Hooked on Performance
01/ 12

Hooked on
Performance

Taming LiveView's JavaScript hooks at scale

02/ 12

A fast response, a busy browser

Page startup sequence
Browser busy with the bundle
ServerResponseHTML arrives quickly Server-rendered HTML can already be visible
BrowserDownloadThe application bundle __PKG__app.js
BrowserParseAll imported JavaScript
BrowserExecuteApp code and its imports
LiveViewConnectSocket joins, hooks mount The live connection and hook behavior wait until here
Jump has 1.5M+ lines of Elixir, 150K+ lines of JavaScript/TypeScript
03/ 12

The eager hook registry

Every import lands in app.js
__PKG__app.js DatePicker EditorHook ReactThing
the shared mega-bundle
04/ 12

lazy-hook

__PKG__app.js DatePickerReactThing
__CHUNK__hook chunk EditorHook
Loads when LiveView mounts the placeholder hook
05/ 12

Setup

1Installnpm install @jump-app/lazy-hook
2Splitesbuild: --format=esm --splitting __PKG__app.js __CHUNK__hook chunk
3Load<script type="module" src={~p"/assets/app.js"}>
Requires ViewHook subclasses LiveView ^1.2.11
06/ 12

Before and after

Before: hook code inside app.js
After: separate hook chunks
Chrome DevTools performance trace before lazy-hook: one long app.js download bar dominates the network track.
Chrome DevTools performance trace after lazy-hook: a shorter app.js download followed by many separate hook chunk requests.
Production Jump application with network and CPU throttling enabled Different time scales: Before axis ≈ 56 s wide · After axis ≈ 16 s wide
07/ 12

The end result

onload event
Before5.4 s
After2.4 s
−56%
Production Jump application, primary landing page, no CPU or network throttling
08/ 12

Loading across a lifecycle

1

Hook mounts and begins importing

2

User navigates away; the hook is destroyed

3

Import resolves

hook element
mounted × initialize widget?
import()
pending
Handling it inside the hook 4 × isDestroyed guards every callback async module-level import cache
09/ 12

The placeholder handoff

With lazy-hook, the whole change
The hook implementation
stays as it is
LiveViewRegisters and mounts the placeholder hookhooks: { EditorHook }
sync
mount
Placeholder hook · ViewHookStarts the importimport("./hooks/editor")
module
arrives
Real hookReplaces the placeholder hook in the registrymounted()
Destroyed while loadingskip initialization
Updates while loadingone pending updated()
10/ 12

Choosing hooks

lazyHook(() => import(...))Good candidates import ... from ...Usually keep eager
Large dependencies on occasional pagesSmall hooks used throughout the app
Editors, charts, specialized pickersHooks requiring immediate setup
The first mount waits for a chunk request.
11/ 12

A cleaner integration

Type shim removed
Thank you, Steffen and the LiveView team
phoenixframework/phoenix_live_view#4324 · types re-exposed in LiveView 1.2.9