Dejavu, Compose, And The Difference Between Performance Wins And Guardrails

Dejavu, Compose, And The Difference Between Performance Wins And Guardrails

When I first decided to try Dejavu, I was hoping for a slightly dramatic story. I wanted it to point at a few screens and tell me, with evidence, that I had been too relaxed about recomposition. I wanted at least one moment where the tool exposed a bad assumption I had missed.

That did not really happen.

Instead, the experience turned into something a little less flashy and a lot more useful: a mix of Compose guardrails, a real Android tooling bug, a standalone repro, and a clearer picture of what recomposition tooling is actually good at in an existing codebase.

So this is not an article about a massive performance win.

It is an article about what happened when I tried to add recomposition assertions to a real app, what broke first, what I learned from fixing it, and why I would still absolutely use Dejavu again.

Why I Wanted To Try Dejavu

I like tools that make invisible UI behavior visible.

That is especially true in Compose, because a lot of bugs do not look like bugs at first. The screen renders. The interaction works. The tests pass. And yet a refactor quietly broadens recomposition, or a state boundary gets fuzzy, or a screen starts doing more work than it used to.

That kind of regression is easy to introduce and easy to miss.

Dejavu appealed to me because it gives you a way to turn those concerns into explicit test contracts. Instead of only asking whether a screen still works, you can also ask whether the right parts of the screen are the ones recomposing.

That difference matters.

If you are doing a Compose upgrade, refactoring a state-heavy screen, or letting AI help with UI code, “it still renders” is not a high enough bar.

The Outcome Was Not What I Expected

I expected one of two outcomes.

Either:

  • Dejavu would quickly reveal some embarrassing recomposition patterns

or:

  • the app would be too awkward for the tool to be worth keeping

What I actually got was somewhere in the middle.

On the screens where Dejavu worked cleanly, the contracts passed. That told me something useful, but not dramatic: the covered screens were already behaving reasonably well. I was not sitting on some hidden recomposition disaster.

So no, this did not produce a big “I found the problem” moment.

But it did produce something I value almost as much:

  • proof that several important screens already behave sanely
  • protection against future regressions
  • a better understanding of where the tool fits and where it does not

That is why I now think of this rollout as guardrail work more than performance work.

The First Problem Was Not My App

The first serious problem was Dejavu itself on Android.

Some screens worked immediately. Others failed with:

Warning: testTag '…' could not be mapped to a composable function

At first, that looked like an app-side issue. Maybe the tagged composables were too large. Maybe the boundaries were wrong. Maybe I needed to split more UI into smaller wrappers.

That was a reasonable theory, so I tried it.

I added tags to cleaner boundaries. I extracted smaller named composables. I kept reducing the size of the tracked regions. Some screens benefited from that, but one important screen still refused to map.

That is usually the point where a tool rollout quietly dies. You tell yourself the screen is “too complex,” accept partial coverage, and move on.

I did not want to stop there because the failure pattern was too consistent.

So I inspected Dejavu itself.

The Android Failure Was More Specific Than It Looked

What I found was subtle.

The activity already had Compose inspection slot tables on the view tree. In other words, some of the raw Compose inspection data Dejavu needed was already present.

But Dejavu was still returning no composition snapshots.

That meant the tag-mapping failure was not really “this screen is impossible to inspect.” It was closer to “Dejavu is looking for the active activity in a way that misses this test setup.”

If the embedded code does not load, open it on GitHub Gist.

If the embedded code does not load, open it on GitHub Gist.

That distinction changed the whole story.

It meant I was no longer asking, “How do I force Dejavu onto this one awkward screen?”

I was asking, “Why does Dejavu lose the active activity if it attaches after the activity is already resumed?”

That is a much better question.

I Built A Tiny Repro Instead Of Arguing Abstractly

Whenever I hit something like this, I prefer moving toward a minimal reproduction instead of staying inside the full app.

Full apps are noisy. There are too many moving pieces, too many excuses, and too many ways for a real problem to get dismissed as app-specific.

So I built a tiny standalone Android repro:

  • one screen
  • two tagged user composables
  • one boolean state change
  • one expectation that the changed card recomposes
  • one expectation that the unrelated top bar stays stable

That repro failed against the published Dejavu artifact with the same mapping warning.

That was the moment the issue became much easier to reason about.

Now the problem was not “my app is weird.”

Now the problem was:

  • here is a tiny Android Compose case
  • here is the exact failure
  • here is the point where Dejavu loses the activity/root context it needs

That kind of repro is much more useful than a long explanation in a bug report.

There Were Really Two Fix Paths

Once I understood the failure mode, two possible fixes emerged.

The first was the blunt one.

If the embedded code does not load, open it on GitHub Gist.

In that version, the Android test rule explicitly seeds the active activity into Dejavu after `enable()`.

It is small. It works. It is easy to explain.

It also feels a little too aware of an internal runtime problem.

The second option was cleaner.

If the embedded code does not load, open it on GitHub Gist.

Instead of teaching the test rule about the problem, Dejavu recovers the already-active activity internally at enable time and backfills its runtime state from there.

That version keeps the fix inside the library instead of pushing special handling into the rule path.

That is the version I prefer as a contribution.

It is still pragmatic, but it respects the library boundary better.

I ended up reducing that failure into issue #51

And the runtime-side fix was accepted in PR #52

That is worth mentioning here because part of what made Dejavu useful in this app was not just adopting the tool, but helping close an Android edge case that made its tag mapping unreliable in this exact setup.

What Dejavu Actually Brought To This App

After the Android runtime issue was understood and patched locally, Dejavu became genuinely usable.

I ended up with recomposition coverage on several screens that mattered, especially around setup, connection, activation, and settings flows.

What that gave me was not a measurable startup win.

It gave me:

  • recomposition contracts I can run on a real device
  • confidence that future refactors do not quietly broaden recomposition
  • a sharper understanding of the screens where state boundaries are already decent

That last point is worth saying clearly because it is easy to undersell.

If a tool does not expose a disaster, that does not mean the work was wasted.

Sometimes the result is that the current implementation is acceptable, and now you have tests proving it.

That is still engineering value.

So Did It Improve Performance?

Not directly.

This kind of rollout is not like fixing a hot startup path or moving blocking work off the main thread. Adding Dejavu does not make the app faster on its own.

What it does is create pressure against regressions.

It turns “I think this screen is still fine” into “I can prove this screen still recomposes within the boundary I intended.”

That is a different kind of improvement.

It is operational improvement. Maintenance improvement. Refactor safety.

And honestly, that is the more realistic value in a mature codebase.

If I had found terrible recomposition patterns, great. I would have fixed them.

But when I did not, the result was still useful. I learned that the covered screens were not hiding an obvious problem, and now I have guardrails to keep them that way.

I Would Still Use Dejavu Again

Absolutely.

In fact, I would like to try it next on a project where I already know some composables are not in great shape.

That would be a better environment for the other side of the story: not “Dejavu gave me guardrails,” but “Dejavu helped me find the actual recomposition mess faster.”

This app just was not that story.

Here, the more interesting result was:

  • the tool was worth integrating
  • the main friction was an Android runtime edge case
  • the screens I covered mostly confirmed decent behavior
  • the long-term value is guarding that behavior, not claiming a flashy performance win

I am completely fine with that outcome.

Not every worthwhile tool adoption needs to end with a huge metric improvement. Sometimes the honest result is that you added a good tool, fixed a real integration issue, and ended up with a safer codebase.

That is still a win.

Closing Thoughts

I started this work expecting Dejavu to either expose a few ugly Compose patterns or prove too awkward to keep.

Instead, it did something more grounded.

It showed me that recomposition tooling can be valuable even when it does not reveal a dramatic flaw. It gave me guardrails on important screens. It helped me isolate and reproduce an Android runtime issue in the library itself. And it clarified the difference between “performance optimization” and “performance confidence.”

If you are considering adding Dejavu to a real Compose app, my advice is simple:

  1. Start with stateful screens where you care about boundaries.
  2. Expect some app-side cleanup around tags and composable structure.
  3. If mapping fails repeatedly, do not assume your screen is the problem.
  4. Build a small repro early.
  5. Treat the first rollout as guardrail work unless it actually proves a performance issue.

That framing makes the tool much easier to evaluate honestly.

And if the first outcome is “my screens are mostly okay,” that is still a result worth keeping.