The Question
FE DesignDesign the Frontend for a Spreadsheet Application
Design the client-side architecture for a scalable spreadsheet application like Google Sheets. Focus on structural component hierarchy, DOM performance under virtualization for high-density matrices, real-time formula computation using dependency graphs, clean state synchronization across components (toolbar, formulas, and cells), and accessibility behaviors within interactive elements.
React
Tailwind CSS
IndexedDB
TypeScript
DOM Virtualization
Questions & Insights
Clarifying Questions
Grid Scale & Volume: What is the target grid size for the MVP?
Assumption: We need to support up to 10,000 rows and 100 columns (10^6 potential cells) running smoothly at 60 FPS scrolling and editing.
Collaborative Features: Is real-time collaborative editing (multiplayer) required for the MVP?
Assumption: No. The MVP is a local-first single-user interactive spreadsheet that auto-saves to a server. However, the system must decouple state mutation from the presentation layer to allow multiplayer integration (e.g., CRDTs) in the future.
Formula Parsing Complexity: What level of formula execution is required?
Assumption: Basic arithmetic operations, coordinate reference tracking (e.g.,
=A1+B2), range aggregations (e.g., =SUM(A1:B10), =AVERAGE(C1:C5)), and cyclic dependency detection are in scope.Styling and formatting requirements: Do we need to support rich text formatting and cell merges?
Assumption: Standard styles (bold, italic, text color, background color, cell alignment) are required. Cell merging is out of scope for the MVP to prioritize performance and layout simplicity.
---
Crash Strategy
To build an enterprise-grade spreadsheet, we must tackle rendering bottlenecks, coordinate systems, and reactive computational graphs.
The Core Bottleneck: Rendering a million DOM cells will crash the browser. We must design a highly optimized virtualized grid rendering system that recycles visible DOM nodes and dynamically translates coordinates on scroll.
Key Architectural Questions:
How do we render thousands of interactive cells without lag? -> We utilize dynamic DOM Virtualization over a custom Canvas. While Canvas is highly performant, standard DOM virtualization allows for native accessibility, easy element selection, standard text copy/paste, and dynamic input focus for the MVP.
How do we evaluate complex cell formulas without locking the main thread? -> We construct an in-memory Dependency Graph. Cells are represented as nodes. When cell A changes, we run a topological sort on its children to evaluate updates reactively, keeping calculations bounded and detecting circular loops instantly.
How do we structure cell state to avoid full-grid re-renders on every keystroke? -> We use a flat Normalized Key-Value state store mapping string coordinates (e.g.,
"A1") to cell objects. Individual cells subscribe strictly to their specific coordinate state using custom selectors, avoiding master-grid re-renders.---
Elite Bonus Points
Web Workers for Formula Computations: Offload parsing and topological sort dependency evaluation to a background Web Worker thread, keeping the main UI thread at 100% responsiveness even during batch sheet recalculations.
Memory Optimization via Typed Arrays: For very large sheets, represent standard cell states and calculated indices using dynamic flat typed arrays (or compressed sparse matrices) to reduce garbage collection pressure.
Visual Grid Layers with Canvas Overlays: Render cell gridlines, row/column borders, and background fills using a lightweight static HTML Canvas underneath virtualized interactive HTML cell elements to dramatically reduce DOM depth and layout thrashing.
Robust Selection Vector Math: Track selection areas using minimum/maximum coordinate vectors ([x_1, y_1] to [x_2, y_2]) to support multi-cell drag-to-select, batch keyboard navigations, and auto-filling formulas.
---
Design Breakdown
Design Summary
Concise Summary
A decoupled local-first frontend architecture featuring a high-performance Virtualized Grid connected to a flat Normalized Grid State Store, coordinated by a Reactive Directed Acyclic Dependency Engine to ensure rapid formula evaluation and instant DOM rendering.
Major Components
AppShell: Root application wrapper rendering global headers, routing, layout scaffolding, and initial loading UI.
SpreadsheetLayout: Structural layout layout component structuring the workspace: formula bar, toolbar, main workspace, and status bar.
SpreadsheetPage: Orchestrator page handling workspace initialization, sheet context state hooks, and backend synchronization lifecycles.
ToolbarContainer: Feature container mapping formatting actions (bold, italic, colors) and undo/redo operations to the active cell selection state.
FormulaBarContainer: Feature container holding the formula input input field, syncing edits directly to the active focused cell.
GridViewportContainer: Feature container managing scroll wrappers, viewport measurements, and virtualized window math.
VirtualGrid: Presentation component calculating visible rows and columns based on scroll offset, allocating position coords.
CellElement: Leaf component rendering a single cell, managing active focus, standard text input editing, styling, and key listeners.
GridDataStore: Application state manager hosting normalized cell values, active selections, focus indices, and historical transaction states.
FormulaEvaluationEngine: Domain-level engine executing mathematical evaluations, building cell dependency graphs, and validating DAG hierarchies.
CUJ Walkthrough
Scrolling through the Sheet:
The user scrolls down in
GridViewportContainer.The container fires scroll events, triggering recalculations of visible rows/cols inside
VirtualGrid.VirtualGrid updates the subset of absolute positioned CellElement components, recycling standard DOM elements and changing coordinate assignments on the fly.Editing a Formula & Auto-recalculating:
User double-clicks
CellElement at coordinate C1, enters =A1+B1, and presses Enter.The value is piped up to the
FormulaBarContainer and committed to the GridDataStore.GridDataStore dispatches a data update to the FormulaEvaluationEngine.FormulaEvaluationEngine compiles the string, registers C1 as dependent on A1 and B1 in its internal DAG, calculates the numerical output, and updates C1's calculated display value.GridDataStore updates, triggering selective re-renders in CellElement elements for C1 and any downstream dependent cells.Simplicity Audit
We reject heavy 2D canvas drawing and custom rendering loop mechanisms for the MVP. Virtualized DOM elements perfectly balance high performance (under 2,000 DOM elements in memory at any point) with native interactive behaviors, out-of-the-box keyboard focuses, simple styling through CSS variable bindings, and standard web screen-reader accessibility.
Architecture Decision Rationale
Why this architecture is the best for this problem: It cleanly decouples computation (Domain Dependency Graph) from state management (Application Store) and UI rendering (Presentation Virtualized Grid). This separation ensures that scrolling performance remains completely decoupled from heavy spreadsheet calculation algorithms.
Requirement Satisfaction:
Functional: Dynamic virtual grid allows seamless row/col expansion; DAG parser ensures reliable spreadsheet math.
Non-Functional: Virtualization guarantees <10\text{ms} rendering cycles, fulfilling 60 FPS scrolling benchmarks.
---
System Diagram
---
Architecture Deep Dive
Presentation Layer
Component Hierarchy
The presentation layout maintains strict containment flow:
AppShell: Handles global authentication, loading skeletons, and routing layout.
SpreadsheetLayout: Structural layout managing top toolbar, sheet tab-bar, formula entry, and grid positioning.
SpreadsheetPage: Coordinates workspace state context providers.
ToolbarContainer / FormulaBarContainer / GridViewportContainer: Feature containers managing direct subscriptions to slice-based stores (active selection range, current active cell coordinate).
VirtualGrid: Calculates the current horizontal and vertical slice of active cells using layout constraints.
CellElement: Leaf component representing individual coordinate nodes.
Interaction Layer
Event Delegation: Since thousands of cells can sit on-screen, click events are delegated to
VirtualGrid using custom data-row and data-col properties, preventing memory leakage from attached listeners.Selection Vectors: Click-and-drag interactions are mapped to range coordinate pairs
(startCol, startRow) and (endCol, endRow). Dynamic bounding boxes are drawn on an overlay layer using CSS transform: translate3d(x, y, 0) scale(w, h) for hardware-accelerated layouts.Accessibility: Cells implement role
gridcell. Keyboard events (ArrowUp, ArrowDown, Tab, Enter) translate focus states internally. Focused cells render invisible overlays bound with aria-live to read output values for screen-readers.Rendering Layer
DOM Virtualization Strategy:
We listen to scroll offsets inside
GridViewportContainer.We calculate indices: \text{startRow} = \lfloor \text{scrollTop} / \text{rowHeight} \rfloor, and \text{endRow} = \text{startRow} + \lfloor \text{viewHeight} / \text{rowHeight} \rfloor. We add safety buffers (e.g., 5 items) to both bounds.
Visible cells are rendered inside a container utilizing
transform: translate3d() using precalculated absolute inline placements.Reconciliation & Updates: Cells utilize
React.memo using strict shallow comparison of properties (only cell data, raw editing text, and selection status). If cell D4 changes, only D4 re-renders.// Virtualization Calculation Interface
interface ViewportBounds {
scrollTop: number;
scrollLeft: number;
viewportWidth: number;
viewportHeight: number;
}
interface RenderRange {
startRow: number;
endRow: number;
startCol: number;
endCol: number;
}
export function computeRenderRange(
viewport: ViewportBounds,
rowHeights: number[], // Custom heights
colWidths: number[], // Custom widths
buffer: number = 5
): RenderRange {
// Binary search to find start indices due to variable row/column sizes
const startRow = Math.max(0, binarySearchOffset(rowHeights, viewport.scrollTop) - buffer);
const endRow = Math.min(rowHeights.length - 1, binarySearchOffset(rowHeights, viewport.scrollTop + viewport.viewportHeight) + buffer);
const startCol = Math.max(0, binarySearchOffset(colWidths, viewport.scrollLeft) - buffer);
const endCol = Math.min(colWidths.length - 1, binarySearchOffset(colWidths, viewport.scrollLeft + viewport.viewportWidth) + buffer);
return { startRow, endRow, startCol, endCol };
}UI Frameworks / Tools: We'll utilize vanilla React paired with lightweight custom Tailwind CSS for inline sizing, and inline styles for dynamic coordinate transformation offsets.
---
Application Layer
Data Fetching Layer
Batch Updates: Changes are serialized into transaction objects
[{ coordinate: "A1", value: "100" }] and sent as patch payloads to avoid large structural writes.Save Debouncing: Keypresses in edit mode are stored locally instantly, but write-back transactions to the server API are debounced by 2\text{ seconds} during active writing pauses.
Offline Sync Queue: When the browser drops connection, operations are queued in local memory and persisted to IndexedDB. Upon recovering network access, the client drains the queue sequentially.
State Management Layer
State Normalization: We store cells in a flat hash-map lookup:
interface CellState {
coordinate: string; // e.g., "B12"
rawValue: string; // e.g., "=SUM(A1:A10)" or "42"
computedValue: string | number;
style: CellStyle;
}
interface GridState {
cells: Record<string, CellState>;
selectedRange: { start: string; end: string } | null;
activeCell: string | null;
isEditing: boolean;
historyStack: UndoRedoHistory;
}Undo/Redo Framework: A custom Command Pattern implementation. Every mutation generates inverse mutations.
UndoRedoHistory manages pointer coordinates inside an array of discrete state deltas to minimize memory footprint.---
Domain Layer
Business Rules & Formula Evaluation Engine
Directed Acyclic Graph (DAG): When a formula is entered, the engine extracts coordinates (e.g., using RegEx
/([A-Z]+[0-9]+)/g). It updates structural links inside an in-memory graph.Cycle Detection: Before rendering calculations, any new formula edge is validated. We check for cyclic pathways using Depth-First Search (DFS) node coloring (White/Gray/Black algorithm). If a cycle is detected, the engine flags a circular loop error value
#REF! and rejects the graph update.Topological Sorting: Formula recomputation runs top-down through calculated dependencies:
class DependencyGraph {
private adjList: Map<string, Set<string>> = new Map(); // Node -> Dependents
public addDependency(child: string, parent: string) {
if (!this.adjList.has(parent)) {
this.adjList.set(parent, new Set());
}
this.adjList.get(parent)!.add(child);
}
public detectCycle(node: string, visited: Set<string>, stack: Set<string>): boolean {
if (stack.has(node)) return true;
if (visited.has(node)) return false;
visited.add(node);
stack.add(node);
const dependents = this.adjList.get(node) || [];
for (const dep of dependents) {
if (this.detectCycle(dep, visited, stack)) return true;
}
stack.delete(node);
return false;
}
}---
Infrastructure Layer
API / Network
Communication Protocol: JSON-over-HTTP REST endpoint for standard dashboard CRUD actions. A real-time SSE (Server-Sent Events) endpoint pushes background cell updates from other users if needed.
Authentication: JWT token storage, sent dynamically via authorization header wrappers inside
APIClient.Storage
IndexedDB: Persistent caching layer. Spreadsheet state is serialized and cached locally. This allows fast boots during subsequent visits without hitting backend APIs first.
---
Wrap Up
Wrap-up
Trade-offs & Evaluation
DOM Virtualization vs. Canvas Rendering:
Trade-off: Canvas handles larger datasets smoothly but requires a complete reimplementation of input boxes, keyboard navigations, font layout metrics, selection outlines, and assistive screen reading.
Verdict: Virtualized HTML elements are best for our MVP. It ensures rapid, accessible UI creation using standard inputs, keeping performance under 16\text{ms} updates up to 10,000 cells on screen.
Client-Side vs. Server-Side Calc Engine:
Trade-off: Computing calculations in the backend secures code formulas and guarantees massive parallelization, but degrades editing feel with 200\text{-}500\text{ms} network latencies.
Verdict: Execute computation entirely on the client-side inside a modular evaluation service class, supporting lightning-fast offline execution for local calculations.
---