See exactly how long your Etherpad takes to become editable — and which assets weigh it down — straight from the browser, with no external APM.
This plugin instruments the client lifecycle: it times every Etherpad boot hook (documentReady → postAceInit) and records the load time and byte size of every resource the pad, outer frame, and inner editor request. The data is exposed as JSON at /stats, ready to graph, alert on, or diff between releases.
- Find the slow phase of pad load. The boot waterfall (
etherpadHooksDuration) shows the cost of each lifecycle stage, so you know whether time goes to the editor, the toolbar, or plugin init — not just a single opaque "load time". - Catch asset bloat before users do. Per-resource decoded/encoded/transfer sizes make it obvious when a font, locale bundle, or plugin asset balloons.
- Spot regressions across releases.
/statsis plain JSON, so you can snapshot it in CI and fail the build when boot time or payload size creeps up. - No third-party APM. Everything is collected with the browser's own
PerformanceResourceTimingAPI and stays on your server.
Open any pad (this is what populates the metrics), then GET /stats. The plugin's output lives under the ep_performance_test_hooks key. Below is a real response from an Etherpad running this plugin — the values are exactly as captured; only explanatory // comments have been added and the loadTimes/loadSizes maps truncated (the full response carries every resource across all three frames — 49 + 49 + 46 in this capture):
| Field | What it tells you |
|---|---|
etherpadHooks |
Wall-clock timestamp each lifecycle hook fired |
etherpadHooksDuration |
Milliseconds from documentReady to each hook — the boot waterfall |
loadTimes.{main,outer,inner} |
Redirect / DNS / TCP / TLS / response timings per resource, for each of the three pad frames |
loadSizes.{main,outer,inner} |
decodedBodySize / encodedBodySize / transferSize per resource |
performance |
The page's navigation PerformanceEntry |
The plugin only emits data — render or alert on it however you like. For example, fail CI if the editor takes too long to become interactive:
curl -s http://localhost:9001/stats \
| jq '.ep_performance_test_hooks.etherpadHooksDuration.postAceInit' # -> 283…or find your heaviest assets:
curl -s http://localhost:9001/stats \
| jq -r '.ep_performance_test_hooks.loadSizes.main
| to_entries | sort_by(-.value.decodedBodySize)[:10][]
| "\(.value.decodedBodySize)\t\(.key)"'Install from the Etherpad admin UI (Admin → Manage Plugins,
search for ep_performance_test_hooks and click Install), or from the Etherpad
root directory:
pnpm run plugins install ep_performance_test_hooks
⚠️ Don't runnpm i/npm installyourself from the Etherpad source tree — Etherpad tracks installed plugins through its own plugin-manager, and hand-editingpackage.jsoncan leave the server unable to start.
After installing, restart Etherpad.
{ "ep_performance_test_hooks": { // Wall-clock timestamp each lifecycle hook fired "etherpadHooks": { "documentReady": 1781864859040, "aceInitInnerdocbodyHead": 1781864859200, "aceInitialized": 1781864859220, "postToolbarInit": 1781864859250, "postAceInit": 1781864859323 }, // The boot waterfall: ms from documentReady to each hook "etherpadHooksDuration": { "documentReady": 0, "aceInitInnerdocbodyHead": 160, "aceInitialized": 180, "postToolbarInit": 210, "postAceInit": 283 // editor interactive 283 ms after documentReady }, // Per-resource timing for the main / outer / inner frames (2 of 49 shown) "loadTimes": { "main": { "/static/css/pad.css": { "redirectTime": 0, "domainLookupTime": 0, "tcpTime": 0, "secureConnectionTime": "0", "responseTime": 0, "fetchUntilResponseEndTime": 0, "requestStartUntilResponseEndTime": 0, "startUntilResponseEndTime": 0 } } }, // Per-resource byte sizes (2 of 49 shown) "loadSizes": { "main": { "/static/css/pad.css": { "decodedBodySize": 9279, "encodedBodySize": 9279, "transferSize": 0 }, "/static/skins/colibris/pad.css": { "decodedBodySize": 2145, "encodedBodySize": 2145, "transferSize": 0 } } } } }