Vi la tre månader med att jaga ett spöke. Varje gång vi trodde att vi hade löst det försvann det, och lämnade oss med samma hackiga UI, men visserligen med en renare och mer robust kodbas.
Det här är storyn om hur våra upprepade misslyckandened att lösa en prestandabugg tvingade oss att plöja igenom mängder av potentiella problem. Vi lyckades fixa en massa saker på vägen, men ingenting rubbade denna buggen.
Vad var det för bugg?
Jo, det var så enkelt som väldigt låg framerate (fps) när ett inputfält, för att kommentera på ett socialt inlägg, skulle flyttas upp längs skärmen när mobilens tangentbord öppnades.
För er som är nya på Flutter eller precis börjat: ett av sätten att hitta problem med låg fps är att titta på widget rebuilds. En widget bygger om sig själv (rebuild) varje gång din kod säger åt den att göra det, eller när Flutter-ramverket bedömer att något i UI:t har ändrats och därför behöver widgeten uppdatera sitt utseende, layout eller position på skärmen.
Som ni ser, ganska dåligt...
Vi började med att analysera widget rebuilds på den här sidan och såg att de var skyhöga när inputfältet öppnades och stängdes. Makes sense. Men varför? Detta är en till synes enkel och standard positionsändring utan att några andra widgets rör på sig. För att förstå orsaken till alla rebuilds använde vi Flutter inspector som har en Performance view där man kan visualisera alla widget rebuilds (glöm inte att bocka i Track Widget Builds).
Exempel på visualisering av widget builds
I vår app såg vi, tyvärr, att hela widget-trädet (även widgets på parent-sidor som inte ens visades) byggdes om på varje frame tills tangentbordet hade avslutat sin animation. Vi kliade oss i huvudet och började ifrågasätta om vi verkligen hade koll på hur Flutter fungerar...
Vid denna tiden använde vi go_router som vi inte var så nöjda med överlag. Vi hade routes som magiska strängar överallt i kodbasen och hade funderat på att hitta något bättre. Vår första hypotes, biased som vi redan var mot go_router, var att vi använde det felaktigt. Vi misstänkte att när tangentbordet ändrade storlek på den översta sidan så sa go_router oavsiktligt åt varje parent i stacken att bygga om sig, vilket vi trodde kunde förklara varför hela applikationen byggdes om. (Tips: Om du vill minska widget rebuilds i din egen app, kolla in den här grymma guiden på FlutterWire: How to Stop Unwanted Widget Rebuilds)
Visualisering av den nästlade sidstrukturen
Så vi tog det klassiska beslutet: refaktorera allt... Vi bestämde oss för att riva ut hela vår routing och ersätta den med auto_route. Eftersom vi ändå inte gillade DX:en med go_router letade vi inte bara efter en prestandafix, vi ville ha en bättre, typsäker lösning för routing, och det var precis vad auto_route erbjöd. Vi spenderade kvällar och helger i över en månad med att refaktorera routingen.
Kolla in den här skönheten:
// Detta gör widgeten till en route
@RoutePage()
class GolfclubSocialPostPage extends StatefulWidget {
const GolfclubSocialPostPage({
// PathParam-attributet markerar dessa som obligatoriska path-parametrar
@PathParam() required this.clubId,
@PathParam() required this.postId,
super.key
});
...
}
// Navigera till sidan såhär:
context.navigateTo(
GolfclubSocialPostRoute(
clubId: clubId,
postId: postId,
),
);
Refaktoreringen från go_router till auto_route var en trevlig upgrade. Koden var mer clean, vi hade typsäkra path-parametrar, och routingen var äntligen begriplig. Meeeen buggen fanns kvar. Vi hade inte hittat spöket, vi hade bara byggt det ett finare hus att bo i...
Att ersätta hela vår navigationsstack hade gett exakt noll prestandaförbättringar. Det var ett ödmjukande ögonblick. Om nu routingen inte orsakade de globala rebuildsen, vad var det då?
Vi gick tillbaka till ritbordet och frågade oss själva: Vad är det egentligen som ändras när ett tangentbord öppnas?
Svaret verkade för enkelt: skärmdimensionerna. I Flutter triggar en ändring av skärmdimensioner en kedjereaktion genom ett specifikt, kraftfullt och ofta missförstått verktyg: MediaQuery.
Läs vidare i Kapitel 2 där vi var tvungna att dyka ner i Flutters interna delar för att avslöja hur en enda rad med Theme-data eller ett felplacerat MediaQuery-anrop kan få även den mest robusta appen på knä.
