Skip to content

perf(core): batch menu viewport measurements - #1823

Merged
sampotts merged 2 commits into
mainfrom
fix/forced-layout
Jul 16, 2026
Merged

perf(core): batch menu viewport measurements#1823
sampotts merged 2 commits into
mainfrom
fix/forced-layout

Conversation

@sampotts

@sampotts sampotts commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Closes #1821

Summary

Opening an ejected light-DOM settings menu previously repeated root sizing for every initially hidden submenu and interleaved per-view style writes with layout reads. That work grew with the number of mounted panels and became expensive against a stylesheet-heavy host page.

This change skips the initial-hidden sync, batches the root and entering or exiting view measurements, and restores temporary styles without a final forced layout. The CSS transition barriers remain so submenu animation behavior is unchanged.

The meaningful changes are concentrated in three areas:

measureViews() (line 167) — the main performance change. It applies measurement styles to all involved views before reading, does at most one constrained-width pass, and restores styles without a final layout flush.

Entering/exiting transition setup (line 257) — entering now measures root + submenu together; exiting does the same at line 311 (line 311). The forceLayout() calls here remain intentional CSS transition barriers.

Hidden lifecycle guard (line 415) — lines 426–430 skip root measurement for initially hidden panels, while retaining the resync when a previously visible panel becomes hidden.

Most changes above line 167 are private-name cleanup, not behavior.

Benchmarks

Reported beta.25 production baseline

Settings panels Geometry reads Forced-layout time
4 76 ~160 ms
13 184 ~400 ms

Current light-DOM Tailwind v4 fixture

Settings panels Geometry reads after change Forced-layout time after change*
4 8 1.8 ms
13 8 2.5 ms

*Measured as elapsed time inside the instrumented getBoundingClientRect() calls, with Tailwind v4 browser CDN CSS and 24 styled host-page cards. This is a controlled local approximation, not a DevTools Layout-event total from the reporter production stylesheet. It confirms the important property: opening cost is constant as mounted panel count grows.

Changes

  • Avoid root sizing when child panels first enter the hidden lifecycle state.
  • Measure transition view pairs in shared natural-size and constrained-size passes.
  • Keep active-to-hidden restoration and CSS transition state barriers intact.

Verification

Lifecycle case Regression assertion
13 initially hidden child panels 0 root-view geometry reads
Natural-size measurement 1 geometry read
Constrained-size measurement 2 geometry reads
Entering submenu Root and submenu measurements share the natural-size pass
Exiting submenu Root and exiting view measurements share the natural-size pass while retaining the two CSS transition barriers

A production reprofile against the reporter host stylesheet remains the final wall-clock validation.

Testing

  • pnpm -F @videojs/core test src/dom/ui/menu/tests/menu-viewport-transition.test.ts
  • pnpm -F @videojs/core build
  • pnpm exec biome check packages/core/src/dom/ui/menu/menu-viewport-transition.ts packages/core/src/dom/ui/menu/tests/menu-viewport-transition.test.ts

@netlify

netlify Bot commented Jul 13, 2026

Copy link
Copy Markdown

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit a74ce23
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/6a543c7ecc3d820008dd01a9
😎 Deploy Preview https://deploy-preview-1823--vjs10-site.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview, Comment Jul 13, 2026 1:17am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

🎨 @videojs/html — no changes
Presets (7)
Entry Initial Lazy
/video (default) 51.00 kB 54.24 kB
/video (default + hls) 190.25 kB 54.24 kB
/video (minimal) 51.02 kB 54.24 kB
/video (minimal + hls) 190.21 kB 54.24 kB
/audio (default) 44.26 kB 54.24 kB
/audio (minimal) 41.47 kB 54.24 kB
/background 4.20 kB
Media (10)
Entry Initial
/media/background-video 1.14 kB
/media/container 1.71 kB
/media/dash-video 214.79 kB
/media/hlsjs-video 141.52 kB
/media/mux-audio 164.00 kB
/media/mux-video 163.70 kB
/media/native-hls-video 9.05 kB
/media/simple-hls-audio-only 17.19 kB
/media/simple-hls-video 18.69 kB
/media/vimeo-video 12.31 kB
Players (5)
Entry Initial
/video/player 8.22 kB
/audio/player 5.38 kB
/background/player 3.92 kB
/live-video/player 7.63 kB
/live-audio/player 5.39 kB
Skins (30)
Entry Type Initial Lazy
/video/minimal-skin.css css 5.70 kB
/video/skin.css css 5.65 kB
/video/minimal-skin js 51.02 kB 54.24 kB
/video/minimal-skin.tailwind js 51.64 kB 54.24 kB
/video/skin js 50.96 kB 54.24 kB
/video/skin.tailwind js 51.77 kB 54.24 kB
/audio/minimal-skin.css css 3.97 kB
/audio/skin.css css 3.86 kB
/audio/minimal-skin js 41.45 kB 54.24 kB
/audio/minimal-skin.tailwind js 42.04 kB 54.24 kB
/audio/skin js 44.27 kB 54.24 kB
/audio/skin.tailwind js 44.82 kB 54.24 kB
/background/skin.css css 133 B
/background/skin js 1.14 kB
/live-video/minimal-skin.css css 5.70 kB
/live-video/skin.css css 5.65 kB
/live-video/minimal-skin js 47.89 kB 54.24 kB
/live-video/minimal-skin.tailwind js 48.45 kB 54.24 kB
/live-video/skin js 49.48 kB 54.24 kB
/live-video/skin.tailwind js 50.11 kB 54.24 kB
/live-audio/minimal-skin.css css 3.97 kB
/live-audio/skin.css css 3.86 kB
/live-audio/minimal-skin js 33.86 kB 54.24 kB
/live-audio/minimal-skin.tailwind js 33.22 kB 54.24 kB
/live-audio/skin js 36.79 kB 54.24 kB
/live-audio/skin.tailwind js 36.29 kB 54.24 kB
/global.css css 183 B
/shared.css css 153 B
/tailwind.css css 161 B
/skin-element js 1.51 kB
UI Components (39)
Entry Initial
/ui/airplay-button 2.54 kB
/ui/alert-dialog 2.90 kB
/ui/alert-dialog-close 2.36 kB
/ui/alert-dialog-description 2.31 kB
/ui/alert-dialog-title 2.25 kB
/ui/audio-track-radio-group 3.03 kB
/ui/buffering-indicator 2.62 kB
/ui/captions-button 2.66 kB
/ui/captions-radio-group 3.03 kB
/ui/cast-button 2.57 kB
/ui/compounds 3.19 kB
/ui/controls 2.96 kB
/ui/error-dialog 2.97 kB
/ui/fullscreen-button 2.57 kB
/ui/hotkey 2.47 kB
/ui/menu 3.00 kB
/ui/mute-button 2.56 kB
/ui/pip-button 2.55 kB
/ui/play-button 2.61 kB
/ui/playback-rate-button 2.63 kB
/ui/playback-rate-radio-group 2.86 kB
/ui/popover 3.13 kB
/ui/poster 2.49 kB
/ui/quality-radio-group 3.00 kB
/ui/seek-button 2.56 kB
/ui/seek-indicator 2.74 kB
/ui/seek-indicator-value 385 B
/ui/slider 2.99 kB
/ui/status-announcer 2.50 kB
/ui/status-indicator 2.64 kB
/ui/status-indicator-value 450 B
/ui/thumbnail 2.56 kB
/ui/time 2.98 kB
/ui/time-slider 3.00 kB
/ui/tooltip 2.86 kB
/ui/volume-indicator 2.57 kB
/ui/volume-indicator-fill 441 B
/ui/volume-indicator-value 514 B
/ui/volume-slider 2.99 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react — no changes
Presets (7)
Entry Initial Lazy
/video (default) 40.56 kB 54.24 kB
/video (default + hls) 178.73 kB 54.24 kB
/video (minimal) 40.94 kB 54.24 kB
/video (minimal + hls) 179.07 kB 54.24 kB
/audio (default) 33.76 kB 54.24 kB
/audio (minimal) 33.80 kB 54.24 kB
/background 579 B
Media (9)
Entry Initial
/media/background-video 403 B
/media/dash-video 212.98 kB
/media/hlsjs-video 139.79 kB
/media/mux-audio 162.38 kB
/media/mux-video 162.41 kB
/media/native-hls-video 7.26 kB
/media/simple-hls-audio-only 15.39 kB
/media/simple-hls-video 16.96 kB
/media/vimeo-video 10.44 kB
Skins (27)
Entry Type Initial Lazy
/tailwind.css css 161 B
/video/minimal-skin.css css 5.55 kB
/video/skin.css css 5.52 kB
/video/minimal-skin js 40.88 kB 54.24 kB
/video/minimal-skin.tailwind js 46.97 kB 54.24 kB
/video/skin js 40.47 kB 54.24 kB
/video/skin.tailwind js 46.63 kB 54.24 kB
/audio/minimal-skin.css css 3.79 kB
/audio/skin.css css 3.68 kB
/audio/minimal-skin js 33.80 kB 54.24 kB
/audio/minimal-skin.tailwind js 35.90 kB 54.24 kB
/audio/skin js 33.68 kB 54.24 kB
/audio/skin.tailwind js 37.92 kB 54.24 kB
/background/skin.css css 90 B
/background/skin js 272 B
/live-video/minimal-skin.css css 5.55 kB
/live-video/skin.css css 5.52 kB
/live-video/minimal-skin js 35.43 kB 54.24 kB
/live-video/minimal-skin.tailwind js 41.46 kB 54.24 kB
/live-video/skin js 35.44 kB 54.24 kB
/live-video/skin.tailwind js 41.55 kB 54.24 kB
/live-audio/minimal-skin.css css 3.79 kB
/live-audio/skin.css css 3.68 kB
/live-audio/minimal-skin js 24.25 kB 54.24 kB
/live-audio/minimal-skin.tailwind js 27.42 kB 54.24 kB
/live-audio/skin js 24.31 kB 54.24 kB
/live-audio/skin.tailwind js 27.45 kB 54.24 kB
UI Components (33)
Entry Initial
/ui/airplay-button 2.45 kB
/ui/alert-dialog 2.60 kB
/ui/audio-track 2.32 kB
/ui/buffering-indicator 2.51 kB
/ui/captions-button 2.55 kB
/ui/captions-radio-group 2.29 kB
/ui/cast-button 2.46 kB
/ui/controls 2.53 kB
/ui/error-dialog 2.58 kB
/ui/fullscreen-button 2.42 kB
/ui/gesture 2.31 kB
/ui/hotkey 2.32 kB
/ui/live-button 2.40 kB
/ui/menu 2.56 kB
/ui/mute-button 2.38 kB
/ui/pip-button 2.50 kB
/ui/play-button 2.49 kB
/ui/playback-rate 2.45 kB
/ui/playback-rate-button 2.47 kB
/ui/popover 3.02 kB
/ui/poster 2.43 kB
/ui/quality 2.26 kB
/ui/seek-button 2.40 kB
/ui/seek-indicator 2.58 kB
/ui/slider 2.70 kB
/ui/status-announcer 2.30 kB
/ui/status-indicator 2.34 kB
/ui/thumbnail 2.46 kB
/ui/time 2.23 kB
/ui/time-slider 2.54 kB
/ui/tooltip 2.97 kB
/ui/volume-indicator 2.33 kB
/ui/volume-slider 2.52 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core — no changes
Entries (68)
Entry Initial Lazy
. 10.39 kB
/dom 17.70 kB
/dom/media/custom-media-element 2.09 kB
/dom/media/dash 208.97 kB
/dom/media/google-cast 4.03 kB
/dom/media/hls-js 135.87 kB
/dom/media/media-host 1.25 kB
/dom/media/media-played-ranges 576 B
/dom/media/mux 151.37 kB
/dom/media/native-hls 3.07 kB
/dom/media/simple-hls 16.48 kB
/dom/media/simple-hls-audio-only 14.93 kB
/dom/media/vimeo 9.87 kB
/media/predicate 563 B
/i18n 2.66 kB 54.24 kB
/i18n/locales/all 30.58 kB
/i18n/locales/ar 1.21 kB
/i18n/locales/az 1.08 kB
/i18n/locales/bg 1.25 kB
/i18n/locales/bn 1.27 kB
/i18n/locales/bs 1014 B
/i18n/locales/ca 1.05 kB
/i18n/locales/cs 1.04 kB
/i18n/locales/cy 1.00 kB
/i18n/locales/da 996 B
/i18n/locales/de 1.08 kB
/i18n/locales/el 1.45 kB
/i18n/locales/en 621 B
/i18n/locales/es 1010 B
/i18n/locales/et 1.05 kB
/i18n/locales/eu 1.01 kB
/i18n/locales/fa 1.20 kB
/i18n/locales/fi 1.03 kB
/i18n/locales/fr 1.07 kB
/i18n/locales/gd 1.10 kB
/i18n/locales/gl 1014 B
/i18n/locales/he 1.12 kB
/i18n/locales/hi 1.28 kB
/i18n/locales/hr 1.02 kB
/i18n/locales/hu 1.09 kB
/i18n/locales/it 1023 B
/i18n/locales/ja 1.17 kB
/i18n/locales/ko 1.11 kB
/i18n/locales/lv 1.08 kB
/i18n/locales/mr 1.28 kB
/i18n/locales/nb 1000 B
/i18n/locales/ne 1.28 kB
/i18n/locales/nl 1013 B
/i18n/locales/nn 996 B
/i18n/locales/oc 1.05 kB
/i18n/locales/pl 1.12 kB
/i18n/locales/pt 1.01 kB
/i18n/locales/pt-BR 1.01 kB
/i18n/locales/pt-PT 1009 B
/i18n/locales/ro 1.05 kB
/i18n/locales/ru 1.33 kB
/i18n/locales/sk 1.10 kB
/i18n/locales/sl 1.02 kB
/i18n/locales/sr 1.03 kB
/i18n/locales/sv 1022 B
/i18n/locales/te 1.31 kB
/i18n/locales/th 1.29 kB
/i18n/locales/tr 1.07 kB
/i18n/locales/uk 1.36 kB
/i18n/locales/vi 1.08 kB
/i18n/locales/zh 1.01 kB
/i18n/locales/zh-CN 1.01 kB
/i18n/locales/zh-TW 1.01 kB
🏷️ @videojs/element — no changes
Entries (2)
Entry Initial
. 996 B
/context 943 B
📦 @videojs/store — no changes
Entries (3)
Entry Initial
. 1.39 kB
/html 696 B
/react 360 B
🔧 @videojs/utils — no changes
Entries (11)
Entry Initial
/array 104 B
/dom 2.74 kB
/events 319 B
/function 327 B
/object 275 B
/predicate 265 B
/percent 281 B
/string 231 B
/style 190 B
/time 629 B
/number 158 B
📦 @videojs/spf — no changes
Entries (4)
Entry Initial
. 4.45 kB
/dom 6.32 kB
/hls 15.40 kB
/background-video 12.85 kB

ℹ️ How to interpret

JS sizes are initial static graph totals (minified + brotli). Lazy dynamic chunks are shown separately when present.

Icon Meaning
No change
🔺 Increased ≤ 10%
🔴 Increased > 10%
🔽 Decreased
🆕 New (no baseline)

Run pnpm size locally to check current initial sizes.

@decepulis decepulis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems sensible!

@sampotts
sampotts merged commit 72e175e into main Jul 16, 2026
26 checks passed
@sampotts
sampotts deleted the fix/forced-layout branch July 16, 2026 22:19
@luwes luwes mentioned this pull request Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Settings menu open forces a synchronous layout ~12x per mounted panel (layout thrashing in menu measurement)

4 participants