Skip to content

Conversation

@kennethkalmer
Copy link
Member

@kennethkalmer kennethkalmer commented Dec 9, 2025

Jira Ticket Link / Motivation

WEB-4845 - Performance optimization to eliminate iOS device overheating

Our website (ably.com) is experiencing severe CPU issues on iOS devices causing overheating complaints. Chrome DevTools Performance profiling identified two critical bottlenecks:

  1. Header component: 71-193ms of forced reflows per scroll event
  2. Expander component: 67-92ms of forced reflows (discovered after fixing Header)

Both components were using synchronous layout queries (getBoundingClientRect(), clientHeight) that force the browser to recalculate layout on every interaction.

Summary of Changes

This PR implements performance optimizations for both components using modern browser APIs:

Header Optimization (useThemedScrollpoints)

New Files:

  • src/core/hooks/use-themed-scrollpoints.ts - Custom hook using IntersectionObserver
  • src/core/hooks/use-themed-scrollpoints.test.ts - Comprehensive unit tests (14 tests)
  • src/core/Header/types.ts - Extracted ThemedScrollpoint type

Modified Files:

  • src/core/Header.tsx - Replaced getBoundingClientRect() with useThemedScrollpoints hook
  • vite.config.mts - Added new hook test to suite
  • package.json / pnpm-lock.yaml - Added @testing-library/react dev dependency

Performance Impact:

  • Before: 71-193ms of forced reflows per scroll
  • After: <2ms overhead
  • 98% improvement

Expander Optimization (useContentHeight)

New Files:

  • src/core/hooks/use-content-height.ts - Custom hook using ResizeObserver

Modified Files:

  • src/core/Expander.tsx - Replaced clientHeight with useContentHeight hook
  • Removed throttled resize listener
  • Added useMemo for performance

Performance Impact:

  • Before: 67-92ms of forced reflows
  • After: <5ms overhead
  • 94% improvement

Pages Affected:

  • /chat - Was 67ms reflows (52% of total) → Now <5ms
  • /pubsub - Was 92ms reflows (96% of total) → Now <5ms

Bug Fix: Themed Scrollpoints

Fixed critical bugs in useThemedScrollpoints:

  1. Initial state bug: IntersectionObserver callbacks only fire on changes, not mount

    • Added setTimeout(0) initial check to detect state on mount
  2. Partial updates bug: Observer only reports changed elements, not all intersecting

    • Added Map to track all currently intersecting elements
    • Re-evaluate ALL intersecting elements on each callback
    • Use "closest to header position" logic for accurate detection

How to Manually Test

1. Performance Testing (Chrome DevTools)

pnpm storybook

Header Test:

  1. Open DevTools → Performance tab
  2. Navigate to "Header > With Themed Scrollpoints"
  3. Record while scrolling
  4. Verify zero getBoundingClientRect calls in scroll handler
  5. Verify <2ms overhead

Expander Test:

  1. Navigate to "Expander > Default"
  2. Record while expanding/collapsing
  3. Verify zero clientHeight reads
  4. Verify <5ms overhead

2. Functional Testing

Themed Scrollpoints:

  • Load "Header > With Themed Scrollpoints"
  • Header should be transparent on load (showing orange gradient)
  • Scroll down → header becomes white background
  • Scroll to dark zone → header becomes dark
  • Scroll back to top → header becomes transparent again

Expander:

  • All expand/collapse animations should work smoothly
  • Dynamic content changes should be handled
  • Nested expanders should work (if applicable)

3. Run Tests

pnpm test

All 203 tests should pass.

Key Optimizations

  1. IntersectionObserver API - Async intersection detection (Header)
  2. ResizeObserver API - Async height tracking (Expander)
  3. requestAnimationFrame batching - Batch state updates
  4. Cached DOM references - Query once on mount
  5. Passive scroll listeners - iOS performance boost
  6. Change detection - Only update state when values change
  7. useMemo optimization - Prevent unnecessary recalculations

Testing

  • ✅ All 203 tests passing
  • ✅ 14 new unit tests for useThemedScrollpoints
  • ✅ Backward compatible - no API changes
  • ✅ TypeScript compilation successful
  • ✅ Linting passes

Merge/Deploy Checklist

  • Written automated tests
  • Commits have clear descriptions
  • Checked for performance regressions (massive improvements, no regressions)
  • Remove temporary markdown docs before merging
  • Squash fixup commit with git rebase --autosquash -i

Notes

  • Version bumped to 17.13.0 for pre-release testing in Voltaire
  • Two temporary commits with markdown docs will be removed before final merge
  • One fixup commit will be squashed during rebase
  • No breaking changes - fully backward compatible

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 9, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Comment @coderabbitai help to get the list of available commands and usage tips.

@kennethkalmer kennethkalmer force-pushed the WEB-4845-performance-fixes branch from f7e1470 to 7607085 Compare January 23, 2026 15:50
kennethkalmer and others added 5 commits January 23, 2026 15:51
Replace expensive getBoundingClientRect() calls with IntersectionObserver API
to eliminate forced reflows during scroll handling.

## Changes

### New Files
- `src/core/hooks/use-themed-scrollpoints.ts` - Custom hook using IntersectionObserver for scrollpoint detection
- `src/core/Header/types.ts` - Extracted ThemedScrollpoint type to avoid circular dependencies
- `src/core/hooks/use-themed-scrollpoints.test.ts` - Comprehensive unit tests for the new hook

### Modified Files
- `src/core/Header.tsx`:
  - Replace scrollpointClasses state with useThemedScrollpoints hook
  - Optimize notice banner visibility logic with cached DOM reference
  - Add passive flag to scroll listener for iOS performance
  - Remove getBoundingClientRect calls from scroll handler
- `vite.config.mts` - Add new hook test to unit test suite
- `package.json` / `pnpm-lock.yaml` - Add @testing-library/react dev dependency

## Performance Impact

- **Before**: 71-193ms of forced reflows per scroll event
- **After**: <2ms overhead (98% improvement)
- **Eliminated**: All getBoundingClientRect calls during scroll
- **Added**: Passive scroll listener for iOS optimization

## Key Optimizations

1. **IntersectionObserver API** - Async intersection detection eliminates forced reflows
2. **Cached DOM references** - Query notice element once on mount, not every scroll
3. **Passive scroll listener** - `{ passive: true }` flag for iOS performance
4. **Change detection** - Only update state when values actually change
5. **requestAnimationFrame batching** - Batch IntersectionObserver callbacks

## Testing

- 14 new unit tests for useThemedScrollpoints hook (all passing)
- All existing tests pass
- Backward compatible - no API changes to HeaderProps or ThemedScrollpoint type

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
This is a temporary commit for the PR description and will be removed before merging.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Cast mock IntersectionObserverEntry objects through 'unknown' to satisfy
TypeScript's strict build mode.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Replace synchronous clientHeight reads with ResizeObserver API to eliminate
forced reflows during expand/collapse and resize events.

## Changes

### New Files
- `src/core/hooks/use-content-height.ts` - Custom hook using ResizeObserver for height tracking

### Modified Files
- `src/core/Expander.tsx`:
  - Replace manual clientHeight reads with useContentHeight hook
  - Remove throttled resize listener (handled by ResizeObserver)
  - Remove problematic useEffect dependency loop
  - Add useMemo for showControls and height calculations
  - Eliminate all forced reflows
- `src/core/hooks/use-themed-scrollpoints.test.ts` - Fix TypeScript linting error (replace `any` with proper type)

## Performance Impact

- **Before**: 67-92ms of forced reflows per mount/resize
- **After**: <5ms overhead (94% improvement)
- **Eliminated**: All synchronous clientHeight DOM queries
- **Pattern**: Follows successful Header optimization approach

## Key Optimizations

1. **ResizeObserver API** - Async height tracking eliminates forced reflows
2. **requestAnimationFrame batching** - Batch ResizeObserver callbacks
3. **useMemo optimization** - Prevent unnecessary recalculations
4. **Removed throttle** - ResizeObserver handles debouncing naturally
5. **Fixed dependency loop** - Removed problematic useEffect with contentHeight dependency

## Testing

- All 203 tests pass (including 5 Expander Storybook tests)
- Backward compatible - no API changes to ExpanderProps
- Linting passes without errors
- TypeScript compilation successful (462 exports)

## Pages Affected (Performance Improvements)

- `/chat` - Was 67ms reflows (52% of total) → Now <5ms
- `/pubsub` - Was 92ms reflows (96% of total) → Now <5ms

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
This is a temporary commit for context and will be removed before merging.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
@kennethkalmer kennethkalmer force-pushed the WEB-4845-performance-fixes branch from 7607085 to 9afd9d0 Compare January 23, 2026 15:51
kennethkalmer and others added 3 commits January 23, 2026 16:39
…rver

fix: themed scrollpoints initial state and intersection detection

Fixes two critical bugs in useThemedScrollpoints hook:

1. **Initial state bug**: IntersectionObserver callbacks only fire on
   changes, not on mount. Added initial check with setTimeout to detect
   which element contains the header position on mount.

2. **Intersection tracking bug**: Observer only reports elements whose
   state changed, not all currently intersecting elements. Added Map to
   track all intersecting elements and re-evaluate best match on each
   callback.

Key changes:
- Initialize activeClassName to "" instead of scrollpoints[0].className
- Add intersectingElementsRef Map to track all intersecting elements
- Find closest element to header position (min distance) in both initial
  check and observer callback
- Update tests to use fake timers and act() for state updates

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants