Skip to content

perf(textarea-autosize): eliminate layout thrashing, cache computed style, replace mousemove with ResizeObserver#48

Open
Copilot wants to merge 8 commits into
mainfrom
copilot/improve-autosize-performance
Open

perf(textarea-autosize): eliminate layout thrashing, cache computed style, replace mousemove with ResizeObserver#48
Copilot wants to merge 8 commits into
mainfrom
copilot/improve-autosize-performance

Conversation

Copilot AI commented Jun 23, 2026

Copy link
Copy Markdown

Three hot-path performance issues in autosize() caused forced synchronous reflows on every keystroke and unnecessary work on every mouse movement.

Changes

1. Batch DOM reads in sizeToFit

All layout reads (overflowOffset, getComputedStyle, container heights) now happen before any writes. Previously, textarea.style.maxHeight was written before getComputedStyle(container).height was read, forcing 2–3 synchronous layout recalculations per keystroke. The only unavoidable write-then-read is the height='auto'scrollHeight measure pattern; it's now isolated with a comment explaining why.

Before (interleaved read/write/read):

textarea.style.maxHeight = `${maxHeight}px`          // write → layout dirty
container.style.height = getComputedStyle(container).height  // forced reflow
textarea.style.height = 'auto'                        // write
textarea.style.height = `${textarea.scrollHeight}px` // forced reflow

After (all reads first, then writes):

containerComputedHeight = getComputedStyle(container).height  // read (no dirty state)
// ... all other reads ...
textarea.style.maxHeight = `${maxHeight}px`           // write
container.style.height = containerComputedHeight      // write
textarea.style.height = 'auto'
const scrollHeight = textarea.scrollHeight            // single unavoidable reflow
textarea.style.height = `${scrollHeight + cachedBorderAddOn}px`

2. Cache getComputedStyle border/box-sizing values

cachedBorderAddOn (derived from borderTopWidth, borderBottomWidth, boxSizing) is computed once and reused across keystrokes. The library-tracked height string is also reused for the maxHeight calculation, avoiding a second getComputedStyle(textarea) call on steady-state keystrokes. Cache is invalidated on external resize and form reset.

3. Replace always-on mousemove with ResizeObserver

  • Modern browsers: A ResizeObserver fires only when dimensions actually change. It compares textarea.style.height against the last library-written value; a mismatch means the user dragged the resize handle → sets isUserResized = true, clears maxHeight, then goes idle.
  • Fallback (no ResizeObserver): The mousemove handler is gated behind mousedown/mouseup (with mouseup on document to handle pointer release outside the element), keeping it off the hot path during normal use.
  • unsubscribe() calls cleanupResizeDetection(), which disconnects the observer or removes all registered listeners.

GitHub Advanced Security started work on behalf of mattcosta7 June 23, 2026 17:44 View session
GitHub Advanced Security finished work on behalf of mattcosta7 June 23, 2026 17:44
…me x/y, ResizeObserver guard, idempotent mousedown)
GitHub Advanced Security started work on behalf of mattcosta7 June 23, 2026 17:49 View session
GitHub Advanced Security finished work on behalf of mattcosta7 June 23, 2026 17:50
GitHub Advanced Security started work on behalf of mattcosta7 June 23, 2026 17:51 View session
GitHub Advanced Security finished work on behalf of mattcosta7 June 23, 2026 17:52
Copilot AI changed the title [WIP] Improve runtime performance of autosize function perf(textarea-autosize): eliminate layout thrashing, cache computed style, replace mousemove with ResizeObserver Jun 23, 2026
Copilot AI requested a review from mattcosta7 June 23, 2026 17:52
@mattcosta7 mattcosta7 requested a review from jonrohan June 29, 2026 14:30
GitHub Advanced Security started work on behalf of mattcosta7 June 29, 2026 18:29 View session
Comment thread test/autosize.test.js
@@ -0,0 +1,180 @@
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot we should run this in ci - generate a workflow for it.

GitHub Advanced Security finished work on behalf of mattcosta7 June 29, 2026 18:30
Copilot stopped work on behalf of mattcosta7 due to an error June 29, 2026 18:30
@mattcosta7 mattcosta7 marked this pull request as ready for review June 29, 2026 19:40
@mattcosta7 mattcosta7 requested a review from a team as a code owner June 29, 2026 19:40
Copilot AI review requested due to automatic review settings June 29, 2026 19:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR optimizes the autosize() hot path to reduce forced synchronous reflows during typing and to avoid always-on pointer-move work, while adding a browser-based test suite to guard regressions.

Changes:

  • Reworks sizeToFit to batch DOM reads before writes and caches border/box-sizing-derived values across keystrokes.
  • Replaces the always-on mousemove resize detection with ResizeObserver (and a gated mouse fallback).
  • Adds Vitest browser-mode tests (Playwright/Chromium) and related configuration.
Show a summary per file
File Description
src/index.js Refactors sizing logic to reduce layout thrashing; adds caching and resize detection via ResizeObserver with fallback cleanup.
test/autosize.test.js Adds browser tests covering grow/shrink, min-height handling, user drag-resize disabling, form reset re-enable, and unsubscribe cleanup.
vitest.config.js Enables Vitest browser mode using the Playwright provider (Chromium headless).
package.json Switches test scripts to Vitest and adds Vitest/Playwright devDependencies.
package-lock.json Locks the newly introduced test/tooling dependency graph.
tsconfig.json Adds skipLibCheck to reduce TS compile overhead.
.gitignore Ignores test screenshot output directory.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 4/8 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment thread package.json
Comment on lines 28 to +32
"eslint": "^7.21.0",
"eslint-plugin-github": "^4.1.2",
"typescript": "^4.2.3"
"playwright": "^1.61.1",
"typescript": "^4.2.3",
"vitest": "^4.1.9"
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.

3 participants