[MUSIC PLAYING]
BEN TRENGROVE: Hi, I'm Ben from the Android Developer Relations
team.
Today, I wanted to show you how I looked into a performance
issue in Jetsnack and how I went about debugging and fixing it
in Jetpack Compose.
So I have here the Jetsnack sample app.
On the details screen, you can see
it has a fancy collapsing toolbar effect, where
as we scroll up, the content moves up,
and as we scroll back down, the content
returns to how it started.
We've had reports that on lower-end devices,
this can appear a little janky as it scrolls.
So let's look into why that could be.
Before we start debugging, though, let's quickly
go over some required knowledge.
Remember, Compose has three phases--
composition, layout, and draw.
Composition determines what to show by building
a tree of composables.
Layout takes those composables and works
out where on the screen they will be shown.
And draw, pretty self-explanatory,
draws everything to the screen.
Here's the cool part.
Compose can skip a phase entirely
if nothing has changed in it.
So there should be no need to recompose just
to relayout a screen.
If we can avoid changing our composition tree,
Compose will skip the composition phase entirely,
and this can lead to performance gains.
This is why our performance documentation states
prefer lambda modifiers when using frequently
changing state.
Using lambdas is what allows work
to be deferred to a later phase and for composition
to be skipped.
But why does this work?
How come just using a lambda modifier means
we can skip composition?
Let's return to the composition tree and find out.
The composition tree is also built up
of any modifiers that are applied to the composables.
Modifiers are effectively immutable objects.
When the translation changes and the modifier is reconstructed,
the old one is removed, and the new one
is added to the composition tree.
This happens every time the translation changes.
Because the composition tree has changed, recomposition occurs.
However, if we use a lambda modifier,
the modifier itself is not actually changing.
Compose is smart enough just to rerun the lambda
when it needs to.
And this is why the composition tree does not change,
and composition can be skipped.
So remember, you shouldn't have to recompose just
to relayout a screen, especially if it's
happening on scroll, which will lead to janky frames.
So whenever you see unnecessary recomposition,
think about how you could move the work to a later phase.
And with that theory out of the way,
let's jump into Android Studio and fix this issue for real.
So in Android Studio, there's a tool called the Layout
Inspector, and the Layout Inspector
can show us the recomposition counts of our composables.
So I'll open it up, and let's have a look.
I'll expand out our list of composables
so we can see what's going on.
And if we scroll the app up and down,
we shouldn't see any recomposition
because the state of the screen is not changing.
It is just being relaid out.
But if we look down at the recomposition counts,
we can see the Title composable was apparently
recomposed 135 times.
And everything else is being skipped.
On the right is the amount of times
a composable has been skipped.
So clearly, something isn't right here.
So if we scroll it up and down again,
we can see this number just keeps going up.
You can also see the recomposition count
in the main window.
So we can see it's now up to 185, so something isn't right.
If we double-click on Title, it will take us
to where it's being used.
And I'll close the Layout Inspector
so we have more room to see.
So Title is reading the scroll state right here,
which means any time the scroll.value changes, all
of these composables have to be recomposed.
Now, the other ones are managing to be skipped,
but let's have a look at Title to see what
it does with that scroll.value.
So it takes in the scroll, and it uses that scroll
to calculate an offset.
And with that offset, it's passing it into a graphicsLayer
modifier, and that's what's doing
the translation on the screen.
And so we know from the theory we just
went over that when we have a frequently changing state,
like the scroll, we should prefer
to use a lambda modifier.
And so graphicsLayer actually has
a lambda form that we can easily switch to in this case.
Here's a catch, though-- it's not
quite enough to fix this issue.
Because we're still reading scroll in composition,
as in, it's not inside a lambda, any time scroll changes,
we're still going to be recomposing here.
So to fix that, what we can do is defer the read.
And to be able to defer a read, we
have to switch the scroll parameter to a lambda as well.
So I'll just call it scrollProvider.
And so now, instead of passing in the scroll.value directly,
we're going to pass in a lambda that gets the scroll.value.
And now we'll still have to move the read into the modifier.
But now that we've moved the read
to here inside the graphicsLayer modifier,
we're reading the state in the draw phase.
The graphicsLayer modifier runs in the draw phase.
And so this should be enough to now allow
us to skip composition.
Of course, we'll have to fix our call site.
But that should be enough to fix the issue.
So let's rerun the app and see if we've achieved that.
So I've rerun the app and opened the Layout Inspector.
And now if we scroll up and down,
we can see no numbers are appearing
at all, which is perfect.
That's exactly what we want.
So now composition is being skipped entirely,
and we did that by deferring the read of this scroll.value
as long as possible.
So we turned it into a lambda here.
And then inside the composable, we
made sure to read it inside our new lambda modifier.
And that's it.
For more performance tips, check out our performance
documentation at this link.
Thank you.