Skip to content

perf(iOS): iOS theme switching is slow (2+ seconds in my testing) #318

Description

@DatScreamer

Summary

Theme switching on iOS took 2+ seconds; it is now perceptually instant, matching Android's theme-switch speed.

Root cause

  1. JS code re-tokenization on every re-render. Code-block syntax highlighting runs highlight.js through JavaScriptCore (HighlightrCodeSyntaxHighlighter.highlightCode) synchronously in the view body. Without memoization, every SwiftUI re-evaluation — streaming deltas, scroll, layout passes, and every theme switch — re-tokenized every visible code block on the main thread. For a code-heavy conversation this alone was multi-second.
  2. Theme forced through identity. themeManager.themeVersion was threaded into .id(...) modifiers and bubble Equatable props, tearing down and rebuilding view subtrees on every theme change instead of recoloring in place.

Changes

  • Memoized Highlightr tokenization (MessageBubbleView.swift): a bounded LRU keyed on (code, language, themeName). Because most Litter themes share one Highlightr palette (e.g. the whole "atom-one-dark" family), switching between same-family themes now hits the cache and simply recolors through the @Observable ThemeStore — no JS work at all. Light↔dark re-tokenizes each visible block exactly once. Also eliminates repeated JS during streaming/scroll.
  • Diff renderer self-updates on theme change (DiffRendering.swift): SyntaxHighlightedDiffText now keys its .task(id:) render inputs on the resolved theme slug, so diffs re-render on theme change without the .id(themeVersion) teardown.
  • Removed theme identity churn: dropped the .id(themeManager.themeVersion) teardowns (settings preview, diff sheet, navigation root) and the dead themeVersion props/Equatable usage in the bubbles. ThemeStore is @Observable, so views that read LitterTheme.* recolor directly — the same pattern Android uses with Compose snapshot state.
  • Dead code cleanup: removed the now-unused themeVersion counter and the unused titleHint/colorScheme params in diff rendering.

Verification

  • Built with the fast simulator lane and switched themes on device: dark↔dark and dark↔light theme changes are perceptually instant (previously 2+ seconds with a visible stall on the open conversation).
  • Time Profiler confirms the highlight.js tokenization is no longer hit on the main thread during a theme switch; code blocks are served from the memoization cache.

Notes

  • No behavior change: same Highlightr palettes, same theme mapping, same colors — just cached and recolored instead of re-tokenized.
  • Follow-up idea: replace Highlightr with a tokenizer decoupled from coloring (like Android's Prism4j with a fixed palette) to also make light↔dark transitions zero-tokenization.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions