Skip to content

Virtualize CSS highlighting for large files - #385

Open
bartveneman wants to merge 6 commits into
mainfrom
claude/css-highlight-virtualization-r301dq
Open

Virtualize CSS highlighting for large files#385
bartveneman wants to merge 6 commits into
mainfrom
claude/css-highlight-virtualization-r301dq

Conversation

@bartveneman

Copy link
Copy Markdown
Member

Summary

Implement viewport-based virtualization for CSS syntax highlighting to improve performance when highlighting large files. Only visible lines are highlighted, reducing DOM operations and memory usage.

Key Changes

  • New module highlight-viewport.ts: Provides viewport tracking and geometry utilities

    • track_viewport_window: Svelte action that monitors scroll position and visible line range, dispatching viewportwindowchange events
    • compute_visible_line_range: Calculates which lines are visible based on scroll offset and line height
    • line_range_to_char_range: Converts line ranges to character offsets for efficient range filtering
    • Helper functions for measuring line height, block size, and finding scroll containers
    • Virtualization only activates for files larger than 250KB threshold
  • Updated use-css-highlight.ts: Integrates viewport tracking into highlighting logic

    • Listens to viewportwindowchange events to update visible range
    • Filters token ranges to only highlight intersecting regions via intersects() helper
    • Skips descending into off-screen AST nodes (e.g., @media blocks) for efficiency
    • Maintains backward compatibility: files below threshold highlight everything
  • Updated HighlightCssCode.svelte: Applies virtualization to component

    • Adds track_viewport_window action to code element
    • Filters CSS Highlight API ranges to visible window
    • Decouples viewport tracking from highlighting via DOM events rather than direct imports

Implementation Details

  • Decoupled architecture: Viewport tracking and highlighting communicate through DOM CustomEvent rather than shared state, enabling independent composition of use: actions
  • Hysteresis: Viewport changes only trigger re-highlighting if the visible window moves by more than half the overscan buffer, reducing thrashing on smooth scrolling
  • Overscan: Defaults to 50 lines beyond visible area to reduce flickering during scroll
  • Fallback behavior: Disabled virtualization for small files and when viewport tracking is disabled
  • Container queries: Uses CSS container query units (cqb) for viewport measurement, allowing future scoping without code changes

https://claude.ai/code/session_01E7X88yKZHKpehyuogXpFDT

@netlify

netlify Bot commented Aug 30, 2026

Copy link
Copy Markdown

Deploy Preview for projectwallace ready!

Name Link
🔨 Latest commit 0239162
🔍 Latest deploy log https://app.netlify.com/projects/projectwallace/deploys/6a954b1378189f0008713418
😎 Deploy Preview https://deploy-preview-385--projectwallace.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.

Comment thread src/lib/components/use-css-highlight.ts Outdated
Comment thread src/lib/components/use-css-highlight.ts
claude added 5 commits August 31, 2026 08:52
For CSS files above ~250KB, only register Custom Highlight API ranges
for tokens/locations within the visible scroll window (plus overscan),
instead of the whole document, so highlight range count and AST-walk
cost stay bounded regardless of file size instead of scaling with it.

The visible window is tracked by a new standalone `track_viewport_window`
Svelte action that measures size via CSS container query units (falling
back to the viewport when no ancestor opts in via `container-type`) and
scroll position via a generic nearest-scrollable-ancestor walk (falling
back to document scroll). It reports changes as a `viewportwindowchange`
CustomEvent on the shared `<code>` node, so it composes with the existing
`highlight_css` action without either one referencing the other directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E7X88yKZHKpehyuogXpFDT
Per review: fold the intersects() guard into a single in_view() check
inside add_range() itself, so call sites (Comment/AtruleName/Property/
Important) go back to their pre-virtualization form instead of each
wrapping the call in its own condition. The AT_RULE/STYLE_RULE subtree
pruning keeps its own in_view() check since it's a distinct decision
(skip descending) from range registration.

Also de-duplicate the schedule(cleanup + do_highlight) pattern used by
both the viewport-change listener and the action's update() into one
rehighlight() function.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E7X88yKZHKpehyuogXpFDT
Per review: raf_id was an unclear abbreviation, renamed to frame_id.
Also switched every single-statement if in this file to always use
braces with the body on its own line, matching what oxlint's `curly`
rule (in its default "all" mode) would enforce.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E7X88yKZHKpehyuogXpFDT
Virtualization already bounds the walk (via SKIP-pruning off-screen
subtrees) and range registration (via in_view()), but do_highlight()
was still calling parse() over the *entire* document on every
scroll-triggered rehighlight, not just on content changes - that's
the one cost left that scales with file size regardless of scroll
position, and the main remaining source of jank on large files.

Cache the parse result (and the line-offset table used to compute
char_range) keyed by (css, node_type), invalidated only when either
actually changes. Comment ranges move from being added during parsing
to a plain array collected once and re-filtered against the current
window on every call, same as the other token types, so they still
correctly track scroll after the walk-only refresh.

Verified in a browser against a ~2.6MB/30k-rule stylesheet: parse()
now runs exactly once regardless of how many times the visible window
changes, and highlighted comment/property/selector ranges still track
the scroll position correctly (offsets move from the top of the file,
to ~50%, to the end, matching each scroll step).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E7X88yKZHKpehyuogXpFDT
Rebased on main, which now enforces oxlint's curly rule repo-wide
(#386). Fixes the handful of one-line if-statements in this branch's
own new code that the rule now flags.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E7X88yKZHKpehyuogXpFDT
@bartveneman
bartveneman force-pushed the claude/css-highlight-virtualization-r301dq branch from 36f3399 to a657a2f Compare August 31, 2026 08:54
The .body scroll container opted into smooth scrolling whenever the
user hadn't set prefers-reduced-motion, which made jumping to a
selected location or the next/previous uncovered coverage block
animate instead of snap. Drop it so scrollTo() always jumps instantly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E7X88yKZHKpehyuogXpFDT
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.

2 participants