The Last Mile: Caching State to Eliminate UI Flicker
Introduction
In the course of building a real-time monitoring dashboard for a GPU-based zero-knowledge proof system, a seemingly minor user experience issue emerged: the status panel was "jumpy." The user reported that the panel flickered when showing "Connecting to cuzk..." and that pipeline state transitions caused visible visual jumps. The assistant's response was a series of four surgical edits to the UI's JavaScript code. The fourth and final edit — the subject of this article — was the simplest of the set, yet it embodied a critical design decision about state lifecycle management in a polling-based monitoring interface.
The subject message reads in its entirety:
Edit 4: Clear cache when polling stops (collapse): [edit] /tmp/czk/cmd/vast-manager/ui.html Edit applied successfully.
This single line, buried in a sequence of four edits, represents the cleanup half of a caching strategy. To understand why it matters, we must trace the reasoning that led to it, the architecture it operates within, and the assumptions it makes about user behavior and system state.
The Problem: DOM Destruction and Visual Flicker
The vast-manager UI is a single-page HTML application that displays a dashboard of GPU instances running proof computations. It has a refresh cycle: every ~10 seconds, the refresh() function fetches fresh dashboard data from a Go backend, calls render() which calls renderInstances(), and the entire instances table is rebuilt by setting innerHTML on a container div. This is a common pattern in simple SPAs — it's straightforward and avoids the complexity of incremental DOM updates.
The cuzk status panel lives inside this instances table. It shows real-time data about proof pipelines: memory usage, synthesis activity, GPU worker states, partition progress, and counters. This data is fetched via a separate polling loop — startCuzkPolling() fires a setInterval every 1.5 seconds that calls the cuzk daemon's HTTP status API through an SSH tunnel.
The problem was a collision between these two cycles. Every time refresh() ran (every ~10 seconds), renderInstances() would destroy the entire instances table DOM, including the cuzk panel div. The panel would be recreated with placeholder text — "Connecting to cuzk..." — and then, up to 1.5 seconds later, the next poll would fill in real data. The result was a visible flash: the panel would disappear, show a loading state, then snap back to populated data. This happened every refresh cycle, creating a persistent "jumpy" feeling.
Additionally, even within the 1.5-second polling cycle itself, the panel content was replaced wholesale via innerHTML, causing layout reflow whenever partition states changed — another source of visual jumpiness.
The Diagnosis: Tracing the Render Cycle
The assistant's reasoning process, visible in the preceding messages ([msg 2640] through [msg 2645]), shows a systematic investigation. The assistant first examined the screenshot the user provided, confirming the UI was rendering but identifying the flicker as the core issue. Then it read the UI HTML file multiple times, tracing the call graph:
refresh()(line 359) → fetches dashboard data → callsrender()→ callsrenderInstances()(line 397)renderInstances()(line 442) → builds the entire instances table HTML string → setsinnerHTMLon the container- Inside the instance row template, when an instance is expanded, a cuzk panel div is created with the text "Connecting to cuzk..."
startCuzkPolling()(line 1396) → sets up a 1.5s interval → fetches cuzk status → callsrenderCuzkPanel()→ setsinnerHTMLon the cuzk panel div The assistant identified the root cause: "Everyrefresh()cycle (10s countdown) callsrenderInstances()which rebuilds the entire table HTML, destroying the cuzk panel and replacing it with 'Connecting to cuzk...' until the next poll (up to 1.5s later)." This is a classic problem in DOM-based UIs: when you rebuild the DOM from scratch, you lose all ephemeral state — including the results of the last API call. The fix required preserving that state across DOM rebuilds.
The Solution: A Four-Edit Caching Strategy
The assistant devised a caching approach with four edits, each addressing a specific point in the lifecycle:
Edit 1 introduced a cache variable — let cuzkLastData = null; — and modified the cuzk container injection to check this cache. If cached data existed, it would be used immediately instead of showing "Connecting to cuzk...".
Edit 2 added the cache-write: on every successful cuzk status fetch, the response was stored into cuzkLastData.
Edit 3 modified renderInstances() to use the cached data when rebuilding the cuzk container. This was the core fix: when refresh() destroyed and recreated the instances table, the cuzk panel would immediately show the last known data instead of the loading placeholder, eliminating the flash.
Edit 4 — the subject message — added cache invalidation: when polling stops (because the user collapses the instance details), the cache is cleared by setting cuzkLastData = null;.
Why Edit 4 Matters: Cache Invalidation as a Design Decision
Edit 4 might look like a trivial cleanup step, but it embodies a crucial design decision: when should cached state be considered stale?
The assistant had to consider several scenarios:
- Collapse and re-expand the same instance: If the user collapses the panel and immediately re-expands it, should they see stale data or a fresh loading state? The decision was to clear the cache on collapse, so re-expanding shows "Connecting to cuzk..." and then fetches fresh data. This is the safer choice — it avoids showing potentially outdated information.
- Switch between instances: The vast-manager manages multiple GPU instances, each with its own cuzk daemon. If the user expands instance A, views its status, collapses it, then expands instance B, the cache from instance A should not be shown for instance B. Clearing the cache on collapse prevents cross-instance data contamination.
- Instance goes away: If an instance is terminated or becomes unreachable, the cached data would show a healthy state that no longer reflects reality. Clearing on collapse ensures the next expansion starts fresh.
- Polling lifecycle: The
stopCuzkPolling()function is called when the user collapses the instance or navigates away. It clears the interval timer and resets the poll UUID. Adding cache clearing here is architecturally coherent — it's the natural place to reset all cuzk-related state. The alternative would have been to keep the cache and only invalidate it on a failed fetch or after a timeout. But that approach is more complex and could lead to showing stale data for longer periods. The assistant chose the simpler, more conservative approach: treat the cache as a short-lived buffer that exists only to bridge the gap between DOM destruction and the next successful poll, not as a long-lived data store.
Assumptions and Their Implications
The caching strategy rests on several assumptions, each worth examining:
Assumption 1: The DOM destruction is the primary cause of flicker. The assistant assumed that eliminating the "Connecting to cuzk..." flash during DOM rebuilds would solve the user's reported jumpiness. This was likely correct for the refresh-cycle flicker, but the user also reported "jumpy on pipeline state" — the visual jumps when partition states update within the 1.5s polling cycle. The caching fix alone doesn't address that second issue; it only prevents the flash during DOM rebuilds. The assistant acknowledged this in its reasoning, noting that CSS fixes like min-height and transitions might also be needed.
Assumption 2: Cached data is always preferable to a loading state. The assistant assumed that showing the last known data (even if it's a few seconds old) is better than showing "Connecting to cuzk..." for up to 1.5 seconds. This is generally true for monitoring dashboards — users prefer continuity over freshness — but it could be misleading if the cached data is significantly out of date. The 1.5-second polling interval mitigates this risk: the cache is never more than 1.5 seconds stale.
Assumption 3: Collapse implies the user is done with that instance. Clearing the cache on collapse assumes the user won't immediately re-expand the same instance. If they do, they'll see "Connecting to cuzk..." again and wait for the first poll. This is a minor regression from the pre-fix behavior (which also showed "Connecting to cuzk..." on every DOM rebuild). The trade-off is acceptable because collapse-and-re-expand is an infrequent operation compared to the automatic refresh cycle.
Assumption 4: The cache variable is scoped correctly. The cuzkLastData variable is declared at the module level (or function scope depending on the exact placement). The assistant assumed that a single global cache is sufficient — that there's only one cuzk panel visible at a time. This holds because the UI shows one expanded instance at a time. If the design later supported multiple expanded instances, the cache would need to be keyed by instance UUID.
Input Knowledge Required
To understand this message, one needs:
- The UI architecture: Knowledge that
refresh()→render()→renderInstances()rebuilds the entire instances table viainnerHTML, destroying all child DOM nodes including the cuzk panel. - The polling architecture: Knowledge that
startCuzkPolling()runs a 1.5s interval to fetch cuzk status, andstopCuzkPolling()clears that interval. - The user's reported symptoms: The screenshot showing the working UI and the description of "jumpy on 'connecting to cuzk'" and "jumpy on pipeline state."
- JavaScript DOM manipulation patterns: Understanding that
innerHTMLreplacement destroys event listeners, state, and DOM state, requiring external caching to preserve data across rebuilds. - The specific codebase: Familiarity with the
renderInstances(),startCuzkPolling(),stopCuzkPolling(), andrenderCuzkPanel()functions and how they interact.
Output Knowledge Created
This message produces:
- A cache invalidation point: The
stopCuzkPolling()function now clearscuzkLastData, ensuring that collapsed panels don't retain stale state. - A coherent lifecycle: The four edits together form a complete caching lifecycle: declare cache → write on fetch → read on render → clear on collapse. Each lifecycle event has a corresponding cache operation.
- A pattern for future fixes: The caching approach demonstrated here could be applied to other parts of the UI that suffer from DOM-rebuild flicker. The pattern is general: cache API responses, use them during DOM reconstruction, invalidate on lifecycle boundaries.
- A testable behavior: After this fix, the cuzk panel should no longer flash "Connecting to cuzk..." during refresh cycles. This is directly verifiable by observing the UI during a refresh.
The Thinking Process: From Symptom to Surgical Fix
The assistant's reasoning, visible across messages [msg 2640] through [msg 2649], shows a clear diagnostic process:
- Observe the symptom: The user reports "jumpy" behavior. The assistant examines the screenshot and confirms the UI is functional but identifies the flicker.
- Hypothesize the root cause: The assistant initially suspects two issues — the "Connecting to cuzk..." flash during DOM rebuilds and the innerHTML replacement causing layout reflow during polls.
- Investigate the code: The assistant reads the UI HTML file multiple times, tracing the call graph from
refresh()throughrenderInstances()to the cuzk panel injection. It uses grep to find all call sites of key functions. - Identify the mechanism: The assistant realizes that
refresh()→render()→renderInstances()destroys the cuzk panel DOM and recreates it with placeholder text, then the next poll fills in real data after up to 1.5 seconds. - Design the fix: The assistant decides on a caching approach — store the last successful response and reuse it when the DOM is rebuilt. This avoids changing the DOM architecture (which would be a larger refactor) while solving the visible symptom.
- Implement in stages: The fix is broken into four edits, each addressing a specific point in the lifecycle. The fourth edit is the cleanup — cache invalidation on collapse.
- Consider edge cases: The assistant considers what happens when polling stops, when the user collapses and re-expands, and when instances are switched. The cache-clearing on collapse handles these cases.
Conclusion
Message [msg 2649] is the quiet capstone of a four-edit fix for a UI flickering bug. Its content is minimal — a single edit command with a one-line description — but its significance lies in the design reasoning it completes. The caching strategy it finalizes is a textbook example of solving a DOM lifecycle problem with application-level state management: cache on write, read from cache during rebuild, invalidate on lifecycle boundaries.
The message also illustrates an important principle in debugging: the simplest fix is often not the most obvious one. Rather than restructuring the DOM update architecture (which would have been a large refactor), the assistant identified a minimal caching layer that bridges the gap between DOM destruction and the next successful API poll. The fourth edit, clearing the cache on collapse, ensures this caching layer doesn't leak state across user interactions — a small but essential piece of the puzzle.
In the broader context of the opencode session, this fix was one of several refinements to the vast-manager UI in segment 20. Alongside fixes for GPU worker idle display bugs, job ID truncation, and ordered partition scheduling, the caching fix made the monitoring dashboard feel polished and responsive — a critical quality for a tool that operators would watch for hours during proof generation runs.