Skip to content

Pre-fetch visual diffs in background with skeleton loading #14

@xinbenlv

Description

@xinbenlv

Original Request

Pre-fetch visual diffs in background with skeleton loading

Context: In packages/core/src/components/DiffBox.vue, the visual diff is fetched from MediaWiki's compare API on-demand when the user switches to the visual diff tab. This blocks the UI for 1-3 seconds on slow networks. Since the review feed already knows the next few revisions, visual diffs for upcoming revisions could be pre-fetched in the background while the user reviews the current edit. Meanwhile, a skeleton/shimmer UI should be shown instead of a blank loading state. This aligns with the project's established pattern of lazy loading with UI skeletons (per CLAUDE.md guidelines). The wikitext diff can be shown immediately as a fallback while the visual diff loads.

Agent's Two Cents (could be wrong)

Everything below is the AI agent's best guess based on the current codebase.
Take with a grain of salt — the original request above is the only thing that came from a human.

Problem / Motivation

DiffBox.vue currently calls MediaWiki's action=compare API with difftype=inline only when the user switches to the visual diff tab or when the revision changes while already on that tab. This creates a synchronous loading bottleneck of 1-3 seconds on slow connections. The review feed (useReviewFeed.ts) already maintains a ranked pool of upcoming revisions via poolRemaining, meaning the next N revisions are known in advance — but their visual diffs are never pre-fetched.

Proposed Solution

  1. Background pre-fetch service: Introduce a composable (e.g., useVisualDiffCache) that watches the review feed's poolRemaining and pre-fetches visual diffs for the top 2-3 upcoming revisions in the background. The cache should be keyed by wiki:revId and have a TTL to avoid stale entries.

  2. Skeleton/shimmer loading UI: Replace the current plain-text "Loading diff..." state in DiffBox.vue with an animated skeleton placeholder that approximates the shape of a diff table (rows of varying-width shimmer bars). This gives immediate visual feedback.

  3. Wikitext fallback: When the user switches to visual diff mode, show the wikitext diff immediately (already available via the diffHtml prop) with a subtle indicator that the visual diff is loading. Swap in the visual diff once it arrives from the cache or network.

Dependencies & Potential Blockers

  • MediaWiki's action=compare API has no documented rate limit for anonymous requests with origin=*, but aggressive pre-fetching (more than 3-5 concurrent requests) could trigger Wikimedia's global rate limiting. Pre-fetch concurrency should be capped.
  • The pre-fetch cache will increase memory usage proportional to the number of cached diffs. A bounded LRU or simple map with TTL eviction is needed.
  • No new external dependencies or credentials required.

How to Validate

  • Open the review feed on a throttled connection (Chrome DevTools → Slow 3G) and verify the visual diff tab loads instantly for the second and subsequent revisions
  • Confirm the skeleton shimmer appears immediately when visual diff is not yet cached (first revision, cold start)
  • Confirm the wikitext diff is shown as fallback while visual diff loads
  • Verify no more than 2-3 concurrent pre-fetch requests are made (check Network tab)
  • Confirm memory does not grow unbounded — cached entries should be evicted after the user moves past them
  • Existing DiffBox.test.ts tests still pass, plus new tests for the cache composable

Scope Estimate

medium

Key Files/Modules Likely Involved

  • packages/core/src/components/DiffBox.vue — add skeleton UI, integrate with cache
  • packages/core/src/composables/useReviewFeed.ts — expose upcoming revisions for pre-fetch
  • packages/core/src/composables/useVisualDiffCache.ts (new) — pre-fetch logic and cache management
  • packages/web/src/pages/FeedPage.vue — wire up pre-fetch composable at the page level
  • packages/core/src/__tests__/components/DiffBox.test.ts — expand tests for skeleton and cache states

Rough Implementation Sketch

  • Create useVisualDiffCache composable that:
    • Accepts a reactive list of upcoming ScoredRevision items
    • Maintains a Map<string, string> of wiki:revId → visual diff HTML
    • Watches the list and fetches diffs for the top N items not yet cached (concurrency-limited)
    • Exposes a getCachedDiff(wiki, revId) method and a reactive has(wiki, revId) check
    • Evicts entries older than a configurable TTL or beyond a max cache size
  • Extract the fetchVisualDiff logic from DiffBox.vue into a shared utility so both the component and the cache can use it
  • In DiffBox.vue, check the cache first before making a network request; add skeleton markup with CSS shimmer animation for the loading state
  • Show diffHtml (wikitext) as an immediate fallback with a "Loading visual diff..." badge overlay

Open Questions

  • Should the pre-fetch happen at the page level (FeedPage/ReviewPage) or inside DiffBox itself? Page-level seems cleaner since DiffBox shouldn't know about the feed.
  • How many revisions ahead should be pre-fetched? 2-3 seems reasonable but may need tuning.
  • Should cached diffs persist to localStorage alongside the pool cache, or stay in-memory only? localStorage would survive page reloads but adds serialization cost for potentially large HTML strings.
  • Should pre-fetching be disabled on metered connections (navigator.connection.saveData)?

Potential Risks or Gotchas

  • MediaWiki's inline diff (difftype=inline) is rendered server-side and can return large HTML blobs for big edits. Caching 3 of these simultaneously could use significant memory.
  • If the user skips through revisions quickly, pre-fetch requests for skipped revisions become wasted bandwidth. An abort controller pattern should cancel in-flight fetches for revisions the user has moved past.
  • The action=compare endpoint occasionally returns empty bodies for certain revision pairs (page creations, deleted revisions). The cache needs to handle these gracefully rather than retrying endlessly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    AgentsCanDoSuitable for autonomous agent pickupenhancementNew feature or requestp3Low priority

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions