We Fixed The Progress Bar: A Jetpack Boost Debugging Story

A woman engineer at a workbench repairs a disassembled progress bar, with a whiteboard showing seven iterations behind her and a monitor displaying a full 100% progress bar

Last week I published a post about Jetpack Boost’s progress bar going backwards. That post was an analysis: what the code does, why the bar behaves oddly, and a commentary on misleading code comments.

This post is the sequel. We actually fixed it.

The Setup

Fixing a WordPress plugin’s JavaScript in production is a bad idea. You need a disposable environment where you can break things, patch files, and reload without consequences. Here is what we built:

We added 20 posts, 7 pages, 5 categories, and 7 active plugins to make the environment realistic enough that CSS generation would take a few seconds. The stock WordPress install with one “Hello World” post finishes so fast you can barely see the bar move.

The development loop was simple: edit the built JavaScript file locally, docker cp it into the container, Ctrl+F5 in the browser, click Regenerate, watch what happens.

Problem 1: The Backward Jumps

The previous post explained this in detail. The progress bar combines two values: how many providers have finished (server side) and how far through the current provider we are (client side). At each provider boundary, the code resets the client-side fraction to zero:

This creates a window where the bar drops backward because the server count has incremented but the client fraction is zero. With 6 providers, you see up to 5 backward jumps.

The Fix

Track a monotonic provider index across the loop. Instead of reporting per-provider progress (step / total), report absolute progress across all providers:

When a provider completes, increment the index and set the boundary:

No resets. No backward jumps. The bar only moves forward.

We tested this across 10+ regeneration cycles. Zero backward movement.

Problem 2: The Bar Never Reaches 100%

This one was harder to find. After fixing the backward jumps, the bar would climb smoothly to about 80% and then the “5 files generated a few moments ago” message would appear. The bar never visually hit 100%.

The first instinct was that the progress values were wrong. They were not. We injected console logging into the patched JavaScript and watched the output:

The state values reached 100%. The progress bar did not. That means the bar was being removed from the page before the browser could paint the 100% frame.

The Root Cause

The progress bar component renders conditionally:

The cssState.status comes from the server. When the last provider’s CSS is saved, the server changes the status from pending to generated. The React mutation response comes back, triggers a re-render, and the progress bar is unmounted.

The timeline looks like this:

The progress bar is controlled by server status, not client state. Our client-side isGenerating flag was irrelevant because the rendering decision happened one level up.

The Fix

Three changes, all on the client:

First, the component that decides whether to show the progress bar now also considers the client-side isGenerating flag:

Second, when isGenerating is true but the server status has already flipped to generated, force the progress to 100:

Third, the onFinished callback holds the progress at 100% for 750 milliseconds before hiding:

The 750ms hold gives the browser time to paint the full bar and gives the user a moment to register that it completed. Not so long that you think “yeah, right” about the 100%.

The Iteration Count

We did not get here in one shot. Here is the version history:

Seven iterations to fix a progress bar. The first five addressed the backward jumps. The last two addressed the 100% cap. The hardest part was not writing the fix; it was understanding that the progress bar’s visibility was controlled by server state, not client state.

What Made This Debuggable

A few things made this tractable:

The source code is open. Jetpack is GPL-2.0 and lives in the Automattic/jetpack monorepo on GitHub. The TypeScript source is readable and well-structured. Without access to the source, we would have been guessing.

Docker made iteration fast. Each test cycle was: edit file, docker cp, Ctrl+F5, click Regenerate. No build step, no deploy pipeline. We patched the built JavaScript directly (the minified production bundle) which let us test changes in seconds.

Console logging answered the right question. When the bar stopped at 80%, the natural question was “is the progress value wrong?” Console logging proved the value was correct and redirected attention to the rendering path. Without that data, we could have spent hours tweaking the calculation formula.

A version stamp prevented cache confusion. We injected a version identifier (“patch v5.2”) into the progress bar’s label text. When the version didn’t appear after a reload, we knew the browser was serving cached JavaScript. This seems trivial, but it saved at least one round of “why isn’t my change working?”

The Source Changes

The fix touches four TypeScript files in the Jetpack monorepo:

Total diff is about 30 lines changed. We plan to submit a PR to the Automattic/jetpack repository.

Lessons

Progress bars have two problems: calculation and visibility. Getting the value right is the obvious problem. Keeping the bar on screen long enough to display the correct value is the subtle one. If the component that renders your progress bar is conditional on server state, and the server state changes before the client can paint, your user will never see 100% no matter how correct your math is.

Patch the built output first, then port to source. Working with the production bundle (one 700KB JavaScript file) was faster than setting up the full monorepo build. Once the fix was verified, porting to TypeScript source was straightforward because we already knew exactly what needed to change.

Iteration beats perfection. Version 1 mostly worked. Version 3 caused a regression. Version 5 was correct but incomplete. Each version taught us something. If we had tried to design the perfect fix upfront, we would still be staring at the code.

Have you fixed a progress bar bug? Or is yours currently going backwards somewhere? Let me know on Bluesky or LinkedIn.