Chrome and Firefox Repaint Differently When CSS Grid Changes
A grid track change is not a single kind of event. Chrome's Blink and Firefox's Gecko decide what to repaint from different signals, so the same grid-template-columns edit can cost noticeably different frame time in each. The practical takeaway is narrow: profile the specific property change in both engines rather than trusting the blanket claim that grid edits repaint everything.
The Grid Repaint Assumption
The folklore goes that touching any grid property forces a full repaint of the container and its children. That is roughly true for some changes and plainly false for others. What gets invalidated depends on which internal structures the engine keeps and how finely it can describe what changed.
Blink and Gecko both avoid repainting the whole document for a track resize. They diverge in how much layout work they redo and which paint chunks they discard. A column count change on a small grid tends to land in the same order of magnitude in both. On a large grid with many positioned children, the gap widens.
The assumption survives because it is safe advice. Telling developers that grid changes are expensive discourages animating them, which is usually correct. The cost of the myth is that it hides real optimization work the engines already do, and it hides cases where one engine is meaningfully cheaper than the other.
It also hides the difference between a change that happens once and a change that happens sixty times a second. A single track resize during a breakpoint switch is cheap enough that the engine choice barely registers. The same resize driven by a pointer or a slider is a different problem, because the invalidation cost compounds across frames and any per-frame overhead the engine cannot amortize becomes visible as jank.
Invalidation in Blink and Gecko
Blink's LayoutNG pipeline tracks layout and paint separately. A change to grid track sizing marks the container's layout object dirty, and paint invalidation sets propagate to the affected descendants. Those sets are built per property, so a gap change and a template change can produce different invalidation footprints even though both are grid properties.
Gecko works through its frame tree and keeps display lists that can be reused when only part of a frame changes. A grid track adjustment invalidates the grid frame's layout, and the display list for unaffected children may survive. Reuse depends on whether the child's position and size actually moved, not merely on the parent being marked dirty.
Neither engine publishes a clean public map from CSS property to dirty bit. The behavior is visible in source and in profiling, which is why the practical advice is empirical. A related piece on this site argues that browser rendering work is shifting to platform teams, and this kind of engine-level variance is exactly the sort of thing those teams end up owning.
There is a structural reason for the divergence. Blink's invalidation sets are computed from the layout tree and carry property-specific tags, so the engine can answer the question of which descendants need repainting without walking the whole subtree in many cases. Gecko's display list reuse is a coarser bet: it keeps a retained list per frame and reuses items whose geometry did not change. Coarse reuse is cheap to maintain and forgiving when layout is stable; fine-grained invalidation is more precise when a single child moves but costs more bookkeeping per node. Neither approach dominates, which is why the same page can profile differently on each.
What Actually Triggers Repaint
Changing grid-template-columns from fixed pixels to fractional units resizes every track and forces children to re-layout. Changing grid-gap alone moves children without necessarily changing their intrinsic sizes, which can leave more of the paint work reusable. The two are not interchangeable costs.
Auto-placement is a separate axis. Adding an item that shifts subsequent auto-placed items invalidates the positions of everything downstream of the insertion point. Explicitly placing items on named lines limits the damage to the items whose line assignments changed. On a dense grid, that difference is measurable in frame time.
Subgrid changes the scope again. When a nested grid inherits track sizing from its parent, a parent track change propagates into the subgrid's layout in both engines. Support and implementation details for subgrid still vary, so test the specific nesting pattern rather than assuming parity.
Compositor layers can mask repaint cost entirely. If a grid item is promoted to its own layer, moving it may skip main-thread paint work. That is a real effect and also a trap, because layer promotion has memory and compositing costs that show up elsewhere.
A concrete way to feel the difference: build a grid of a few hundred small cells, each with a border and a background, and animate the column count by one track per frame. In Chrome the paint flashing overlay tends to light up a band along the affected tracks and their children. In Firefox the highlighted region often tracks the moved children more tightly, because the retained display list for unmoved cells survives. The frame numbers you record will vary with hardware and content, but the shape of the invalidation is usually consistent enough to recognize.
Observing the Difference in DevTools
Chrome's paint flashing overlay draws invalidation rectangles over the regions it will repaint. Firefox has a comparable overlay with different rendering and different granularity. Toggling the same class in both and comparing the highlighted area is the fastest way to see the divergence without reading engine source.
Frame timing tells the other half of the story. The paint rectangles show where work happens; the performance panel shows how long it takes. A small invalidation rectangle can still be expensive if it covers a region with heavy shadows or filters, and a large one can be cheap if the content is simple.
Layout shift is the third signal. A grid change that moves content after first paint affects cumulative layout shift regardless of repaint cost. Optimizing repaint while ignoring shift trades one user-visible problem for another. Measure both, and treat them as separate budgets.
Record the same interaction, not two different ones. It is easy to compare a Chrome trace of a hover state against a Firefox trace of a click and draw a wrong conclusion. Script the toggle, or use the same keyboard shortcut in both browsers, and capture a few seconds either side of the change so the steady state is visible alongside the transition.
Practical Implications for Developers
Animating grid-template-columns is the case most likely to disappoint. The interpolation forces layout on every frame in both engines, and the per-frame cost scales with the number of tracks and children. Animating a transform on a child is usually cheaper, though it changes the visual result.
will-change on grid items deserves restraint. It can promote an element and reduce paint work for movement, but it also pins memory and can hurt scrolling on low-end devices. Apply it to the one or two items that actually animate, not to every grid child as a precaution.
Cross-engine testing is not optional here. A change that profiles cleanly in Chrome may produce a wider invalidation region in Firefox, or the reverse. The engines are close enough that most sites never notice, and different enough that an animated dashboard will.
There is a real trade-off between precision and simplicity in how you structure the grid. Named-line placement and explicit track definitions give both engines more to work with, because the layout is more constrained and the engine can reason about which items are affected. Relying on auto-placement everywhere is simpler to write and produces wider invalidation when items shift. The choice is not free either way: explicit placement is more code to maintain, and it can make responsive reflow harder when the number of items changes at runtime.
The objection worth taking seriously: if your grid is small and changes rarely, none of this matters. The overhead of profiling and layer tuning can exceed the frame time you save, and premature optimization here is real. Reserve the effort for grids that animate or resize continuously.
Actions for Cross-Browser Consistency
Profile the exact property change in both Chrome and Firefox before shipping an animated grid. Use the paint flashing overlay and the performance panel together, and record the same interaction in each browser.
Avoid animating grid-template-columns and grid-template-rows for anything that runs continuously. If a layout must move smoothly, animate a transform on a wrapper and keep the grid structure static.
Isolate repaint-heavy grid items into their own stacking context so invalidation does not spread to siblings. A positioned wrapper with a transform does this without touching the grid definition itself.
Report engine-specific invalidation bugs with a minimal test case and a screen recording of the overlay. A reduced page with one grid and one toggled class is far more actionable than a link to a full application, and this site has argued before that long-lived build and rendering code is where these fixes tend to accumulate.
Keep a small harness page per project that toggles the grid properties you actually animate, and run it after major browser updates. Engines change invalidation behavior between releases, and a harness catches the regression before users do.
Finally, write down what you measured. A short note in the repo recording the browser versions, the grid shape, and the frame times turns a one-off profiling session into something the next person can compare against. Invalidation behavior is not a fixed contract, and the only defense against silent regressions is a baseline you can re-run.