Skip to content

Fix Firefox performance#61

Merged
ThomasJButler merged 5 commits intomainfrom
v3.5-DependencyUpdate
Mar 29, 2026
Merged

Fix Firefox performance#61
ThomasJButler merged 5 commits intomainfrom
v3.5-DependencyUpdate

Conversation

@ThomasJButler
Copy link
Copy Markdown
Owner

@ThomasJButler ThomasJButler commented Mar 29, 2026

Summary

  • Firefox performance fixes: The site was never tested on Firefox and performed poorly due to expensive CSS effects (backdrop-filter, will-change overuse, filter animations) and unthrottled canvas animations (MatrixRain shadowBlur, ParticleBackground O(n²) calculations). Adds Firefox detection and targeted optimizations that significantly reduce rendering cost without visible quality loss.
  • General perf fixes (all browsers): Replaces wasteful setInterval in CRTEffect with RAF, eliminates layout thrashing in useParallax scroll handler, fixes anime.js v4 API mismatch in PageTransition.
  • CLAUDE.md: Adds project guidance file for Claude Code with build commands, architecture overview, and code style conventions.

Files changed

File Change
CLAUDE.md New — project guidance for Claude Code
src/main.tsx Firefox detection (sets is-firefox class)
src/css/firefox-performance.css New — centralized Firefox CSS overrides
src/css/main.css Imports firefox-performance.css
src/components/MatrixRain.tsx Disable shadowBlur, throttle 30fps, fewer columns
src/components/ParticleBackground.tsx Half particles, throttle 30fps, shorter connections
src/components/CRTEffect.tsx Replace setInterval with RAF tick
src/components/PageTransition.tsx Opacity flash on Firefox, fix anime.js API
src/hooks/useScrollAnimation.ts Cache layout values, RAF debounce, passive listener

Test plan

  • npm run build passes
  • Open site in Firefox — verify smooth scrolling, no janky animations
  • Open site in Chrome — verify no visual regressions
  • Check Matrix rain still renders on both browsers
  • Check particle background still renders on both browsers
  • Verify header/footer backgrounds look acceptable without backdrop blur on Firefox

Summary by CodeRabbit

  • Style

    • Added Firefox-specific CSS overrides providing performance tuning for visual effects and animations.
  • Refactor

    • Improved animation rendering in CRT effects and page transitions.
    • Optimized scroll event handling with frame-aware throttling mechanisms.
    • Added browser detection for adaptive rendering across components.
  • Documentation

    • Added comprehensive Claude Code guidance documenting project structure and development conventions.

…as animations

- Add Firefox detection via is-firefox class on <html>
- Create firefox-performance.css with targeted overrides:
  - Disable backdrop-filter (replaced with opaque backgrounds)
  - Remove persistent will-change that causes memory bloat
  - Simplify filter animations and text-shadow glows
  - Limit wildcard transitions to interactive elements only
- MatrixRain: disable shadowBlur, throttle to 30fps, reduce columns 30%
- ParticleBackground: halve particle count, throttle to 30fps, reduce
  connection distance (cuts O(n²) calculations significantly)
- CRTEffect: replace wasteful setInterval with RAF-based tick
- useScrollAnimation: cache layout values in useParallax to eliminate
  getBoundingClientRect() layout thrashing on scroll, add RAF debouncing
- PageTransition: use opacity flash instead of filter animation on
  Firefox, fix pre-existing anime.js v4 API mismatch
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 29, 2026

📝 Walkthrough

Walkthrough

This pull request introduces Firefox browser detection and corresponding performance optimizations across the application. A new guidance document is added for architectural reference. Animation loops are refactored from interval-based to RAF-driven approaches, component rendering is conditionally tuned for Firefox (particle counts, frame throttling, visual effects), and a new Firefox-specific CSS stylesheet disables expensive effects like backdrop-filter and global transitions, replacing them with opaque alternatives.

Changes

Cohort / File(s) Summary
Documentation
.claude/CLAUDE.md
New guidance file describing repository architecture, npm scripts, routing, CSS organization, animation libraries, and deployment workflow.
Firefox Detection Setup
src/main.tsx
Adds runtime browser detection to set is-firefox CSS class on document element before React initialization.
Component Performance Optimizations
src/components/CRTEffect.tsx, src/components/MatrixRain.tsx, src/components/PageTransition.tsx, src/components/ParticleBackground.tsx
Refactors flicker animation to RAF-driven tick function; adds Firefox detection with conditional particle density, frame throttling (~30fps), reduced shadow effects, and simplified opacity-based transition animations on Firefox.
CSS Performance Overrides
src/css/firefox-performance.css, src/css/main.css
New stylesheet with Firefox-specific performance rules disabling backdrop-filter, global transitions, and heavy visual effects; applying opaque backgrounds and will-change: auto hints. Main CSS imports the new file.
Scroll Animation Hook Optimization
src/hooks/useScrollAnimation.ts
Caches element dimensions (offsetTop, offsetHeight) and updates only on resize; throttles scroll computations via RAF with rafPending flag; registers scroll listener as passive.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰✨ Firefox hops through now with speed,
No blur or glow when performance we need,
RAF ticks smoothly, particles dance light,
Browser detection—optimization done right! 🚀

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title 'Fix Firefox performance' clearly and directly summarizes the primary objective of the changeset—Firefox-specific performance optimizations across multiple components.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch v3.5-DependencyUpdate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/main.tsx (1)

7-8: Consider extracting Firefox detection to a shared utility.

The navigator.userAgent.includes('Firefox') check is duplicated in four places. A shared utility would improve maintainability if detection logic needs updating.

♻️ Optional: Create shared utility
// src/utils/browserDetection.ts
export const isFirefox = typeof navigator !== 'undefined' && navigator.userAgent.includes('Firefox');

Then import and reuse in main.tsx and component files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.tsx` around lines 7 - 8, Extract the repeated Firefox check into a
shared utility (e.g., export a constant/function named isFirefox) and replace
all occurrences of navigator.userAgent.includes('Firefox') with imports of
isFirefox; ensure the utility guards against SSR by checking typeof navigator
!== 'undefined', export it (isFirefox) from a central module, and update
main.tsx and the other components to import and use isFirefox instead of
duplicating the userAgent check.
src/css/firefox-performance.css (1)

115-130: Scope the global transition kill-switch more narrowly.

Line 115 disables transitions for all descendants with !important; non-whitelisted interactive elements (label, select, details, summary) lose transition behavior. Prefer targeted suppression of expensive properties instead of a universal reset.

Affected elements not in whitelist - `` (ContactPage.tsx, BlogPage.tsx, ContactForm.tsx) - `` (BlogPage.tsx) - `` / `` (ErrorBoundary.tsx) 🤖 Prompt for AI Agents Verify each finding against the current code and only fix it if needed. In `@src/css/firefox-performance.css` around lines 115 - 130, Replace the overly broad ".is-firefox * { transition-property: none !important; }" rule with a targeted suppression of only expensive properties (e.g. transform, opacity, filter, box-shadow, backdrop-filter) so you don't strip transitions from innocuous elements, and add the missing interactive elements to the whitelist: include .is-firefox label, .is-firefox select, .is-firefox details, and .is-firefox summary in the same selector group as .is-firefox a, .is-firefox button, .is-firefox input, .is-firefox textarea, .is-firefox header, .is-firefox [class*="card"], .is-firefox [class*="btn"], .is-firefox .menu-toggle, .is-firefox nav ul; keep the existing transition-duration and the !important only on the targeted properties so performance-sensitive transitions are disabled while preserving expected transitions on label/select/details/summary. 🤖 Prompt for all review comments with AI agents Verify each finding against the current code and only fix it if needed. Inline comments: In `@src/css/firefox-performance.css`: - Around line 93-95: The selector .is-firefox [data-theme="matrix"] body::after is unreachable because data-theme is set on the same element as .is-firefox; update the selector to combine them on the same element (e.g. .is-firefox[data-theme="matrix"] body::after or html.is-firefox[data-theme="matrix"] body::after) so the scanline opacity override actually matches and applies. In `@src/hooks/useScrollAnimation.ts`: - Around line 182-191: The visibility calculation uses a stale windowHeight captured once; modify updateLayout() to also refresh windowHeight = window.innerHeight (in addition to elementTop and elementHeight) and ensure updateLayout is invoked on window resize by adding a resize event listener that calls updateLayout (and remove the listener in the cleanup). Update references to elementTop, elementHeight, windowHeight, and updateLayout accordingly so visibility checks use the current window height. --- Nitpick comments: In `@src/css/firefox-performance.css`: - Around line 115-130: Replace the overly broad ".is-firefox * { transition-property: none !important; }" rule with a targeted suppression of only expensive properties (e.g. transform, opacity, filter, box-shadow, backdrop-filter) so you don't strip transitions from innocuous elements, and add the missing interactive elements to the whitelist: include .is-firefox label, .is-firefox select, .is-firefox details, and .is-firefox summary in the same selector group as .is-firefox a, .is-firefox button, .is-firefox input, .is-firefox textarea, .is-firefox header, .is-firefox [class*="card"], .is-firefox [class*="btn"], .is-firefox .menu-toggle, .is-firefox nav ul; keep the existing transition-duration and the !important only on the targeted properties so performance-sensitive transitions are disabled while preserving expected transitions on label/select/details/summary. In `@src/main.tsx`: - Around line 7-8: Extract the repeated Firefox check into a shared utility (e.g., export a constant/function named isFirefox) and replace all occurrences of navigator.userAgent.includes('Firefox') with imports of isFirefox; ensure the utility guards against SSR by checking typeof navigator !== 'undefined', export it (isFirefox) from a central module, and update main.tsx and the other components to import and use isFirefox instead of duplicating the userAgent check. 🪄 Autofix (Beta) Fix all unresolved CodeRabbit comments on this PR: [ ] Push a commit to this branch (recommended) [ ] Create a new PR with the fixes ℹ️ Review info ⚙️ Run configuration Configuration used: defaults Review profile: CHILL Plan: Pro Run ID: a8ae6e76-0096-4a40-b417-2485d165cee9 📥 Commits Reviewing files that changed from the base of the PR and between 68da9d1 and d5251b5. 📒 Files selected for processing (9) .claude/CLAUDE.md src/components/CRTEffect.tsx src/components/MatrixRain.tsx src/components/PageTransition.tsx src/components/ParticleBackground.tsx src/css/firefox-performance.css src/css/main.css src/hooks/useScrollAnimation.ts src/main.tsx

Comment on lines +93 to +95
.is-firefox [data-theme="matrix"] body::after {
opacity: 0.2;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== data-theme assignments in source =="
rg -nP 'data-theme|setAttribute\(\s*["'\'']data-theme["'\'']|dataset\.theme' --type=ts --type=tsx --type=js --type=jsx --type=html src

echo
echo "== matrix + body::after selectors =="
rg -nP '\[data-theme="matrix"\].*body::after|body\[data-theme="matrix"\]::after|body::after' --type=css src/css

Repository: ThomasJButler/thomasjbutler.github.io

Length of output: 164


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Searching for data-theme assignments =="
rg 'data-theme' src --type=ts --type=js --type=html -n

echo ""
echo "== CSS selectors involving data-theme in firefox-performance.css =="
cat -n src/css/firefox-performance.css | grep -A2 -B2 'data-theme' || echo "Not found with grep, checking with ripgrep..."
rg 'data-theme' src/css/firefox-performance.css -n

echo ""
echo "== Check main.tsx for is-firefox class application =="
rg 'is-firefox' src -n

echo ""
echo "== Check for body element with data-theme =="
rg 'body.*data-theme|data-theme.*body' src --type=ts --type=js --type=html -n

Repository: ThomasJButler/thomasjbutler.github.io

Length of output: 4195


Fix unreachable CSS selector for scanline opacity override.

Line 93 (.is-firefox [data-theme="matrix"] body::after) is unreachable because data-theme is applied to the html element itself (via document.documentElement.setAttribute('data-theme', theme) in ThemeContext.tsx), not to a descendant. The current selector tries to match [data-theme="matrix"] as a descendant of .is-firefox (which is the html element), making it impossible to match.

Proposed fix
-.is-firefox [data-theme="matrix"] body::after {
+.is-firefox[data-theme="matrix"] body::after {
   opacity: 0.2;
 }

Combine the .is-firefox and [data-theme="matrix"] selectors on the same element (html) since both conditions are applied to it.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.is-firefox [data-theme="matrix"] body::after {
opacity: 0.2;
}
.is-firefox[data-theme="matrix"] body::after {
opacity: 0.2;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/css/firefox-performance.css` around lines 93 - 95, The selector
.is-firefox [data-theme="matrix"] body::after is unreachable because data-theme
is set on the same element as .is-firefox; update the selector to combine them
on the same element (e.g. .is-firefox[data-theme="matrix"] body::after or
html.is-firefox[data-theme="matrix"] body::after) so the scanline opacity
override actually matches and applies.

Comment on lines +182 to +191
// Cache layout values to avoid getBoundingClientRect() on every scroll frame
let elementTop = element.offsetTop;
let elementHeight = element.offsetHeight;
const windowHeight = window.innerHeight;
let rafPending = false;

const updateLayout = () => {
elementTop = element.offsetTop;
elementHeight = element.offsetHeight;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

windowHeight is not updated on resize, causing stale visibility calculations.

The windowHeight is captured once at line 185 but updateLayout() (lines 188-191) only updates elementTop and elementHeight. After a window resize, windowHeight remains stale, making the visibility check at line 199 incorrect.

🐛 Proposed fix
+    let windowHeight = window.innerHeight;
-    const windowHeight = window.innerHeight;
     let rafPending = false;

     const updateLayout = () => {
       elementTop = element.offsetTop;
       elementHeight = element.offsetHeight;
+      windowHeight = window.innerHeight;
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useScrollAnimation.ts` around lines 182 - 191, The visibility
calculation uses a stale windowHeight captured once; modify updateLayout() to
also refresh windowHeight = window.innerHeight (in addition to elementTop and
elementHeight) and ensure updateLayout is invoked on window resize by adding a
resize event listener that calls updateLayout (and remove the listener in the
cleanup). Update references to elementTop, elementHeight, windowHeight, and
updateLayout accordingly so visibility checks use the current window height.

@ThomasJButler ThomasJButler changed the title Fix Firefox performance and add CLAUDE.md Fix Firefox performance Mar 29, 2026
@ThomasJButler ThomasJButler merged commit 34e3dbf into main Mar 29, 2026
1 check passed
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.

1 participant