[{"data":1,"prerenderedAt":4},["ShallowReactive",2],{"post-content-flutter-listview-jank-scroll-performance-fixes":3},"\u003Cfigure>\n  \u003Cimg src=\"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1512941937669-90a1b58e7e9c?w=1200&q=80&auto=format&fit=crop\" alt=\"A phone lying on a desk with its screen glowing, the kind of device testers love to find jank on\" loading=\"lazy\" \u002F>\n\u003C\u002Ffigure>\n\n\u003Cp>A tester on a three-year-old budget Android filed a bug last week: our chat screen scrolled \"like a slideshow\". On my Pixel it was fine, so I nearly closed it as can't-reproduce. Then I ran the app on a cheap device myself and watched the frame chart turn into a bar code. The list was the problem, and it was every problem at once.\u003C\u002Fp>\n\n\u003Cp>Fixing scroll jank in Flutter lists is less about clever tricks and more about removing work you never meant to do. Here are the five fixes I now run through before touching anything exotic. All of them apply to GridView and custom scrolls too.\u003C\u002Fp>\n\n\u003Ch2>1. Stop building the whole list\u003C\u002Fh2>\n\n\u003Cp>The default \u003Ccode>ListView\u003C\u002Fcode> constructor builds every child immediately, including the 990 rows you cannot see yet. Ten items, no issue. Five hundred, and your first frame is gone.\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-dart\">\u002F\u002F Builds all 500 widgets up front\nListView(\n  children: messages.map((m) =&gt; MessageTile(message: m)).toList(),\n)\n\n\u002F\u002F Builds a tile only when it scrolls into view\nListView.builder(\n  itemCount: messages.length,\n  itemBuilder: (context, index) =&gt; MessageTile(message: messages[index]),\n)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>This one swap took our first-build time from roughly 400ms to under 40. If you remember nothing else from this post, remember this.\u003C\u002Fp>\n\n\u003Ch2>2. Give the layout pass a break with itemExtent\u003C\u002Fh2>\n\n\u003Cp>If your rows are all the same height, say it out loud:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-dart\">ListView.builder(\n  itemCount: messages.length,\n  itemExtent: 64.0,\n  itemBuilder: (context, index) =&gt; MessageTile(message: messages[index]),\n)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>With \u003Ccode>itemExtent\u003C\u002Fcode> set, Flutter skips per-child layout negotiation and can compute scroll positions by simple multiplication. It reads like a micro-optimization. On a long list it is not micro.\u003C\u002Fp>\n\n\u003Ch2>3. The shrinkWrap trap\u003C\u002Fh2>\n\n\u003Cp>I see this one in every codebase I inherit:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-dart\">Column(\n  children: [\n    Header(),\n    ListView(\n      shrinkWrap: true,\n      children: items.map(buildTile).toList(),\n    ),\n  ],\n)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>\u003Ccode>shrinkWrap: true\u003C\u002Fcode> tells the list to measure all of its children so it can hug them. Which means it just built and laid out every row, exactly like the lazy constructor you avoided. You paid for laziness and got none of it.\u003C\u002Fp>\n\n\u003Cp>If the list should fill the remaining space, wrap it in \u003Ccode>Expanded\u003C\u002Fcode>. If it genuinely needs to be a small scrolling section inside other content, that is what slivers are for. Either way, \u003Ccode>shrinkWrap\u003C\u002Fcode> on a big list is a bug wearing a helpful expression.\u003C\u002Fp>\n\n\u003Ch2>4. Do less work while scrolling\u003C\u002Fh2>\n\n\u003Cp>The builder runs on every frame that brings items in. Whatever you do inside it, you do during a scroll. A few habits that matter:\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>Decode images before they hit the list, and give \u003Ccode>Image\u003C\u002Fcode> widgets explicit sizes so the layout does not guess.\u003C\u002Fli>\n  \u003Cli>Make item widgets \u003Ccode>const\u003C\u002Fcode>-friendly. Constant tiles get skipped on rebuild instead of rebuilt.\u003C\u002Fli>\n  \u003Cli>Do not \u003Ccode>watch\u003C\u002Fcode> an entire app-wide provider from inside a tile. One changed field then rebuilds 50 visible rows. I learned this the hard way back when I \u003Ca href=\"\u002Fposts\u002Fflutter-state-management-3-rewrites\u002F\">rewrote our state management three times\u003C\u002Fa>.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>One nuance people get wrong: \u003Ccode>ListView.builder\u003C\u002Fcode> already wraps each item in a repaint boundary by default (\u003Ccode>addRepaintBoundaries\u003C\u002Fcode> is true). Sprinkling more of them rarely helps. What does help is making sure an animated or filtered widget inside one tile does not repaint that whole tile every tick.\u003C\u002Fp>\n\n\u003Ch2>5. Measure before and after\u003C\u002Fh2>\n\n\u003Cp>Guessing at jank is a waste of an afternoon. Open DevTools, go to the Performance view, record a scroll, and look at the frame chart. Red bars are dropped frames; expand one and the timeline tells you whether the time went to build, layout, or paint. There is also a widget rebuild tracker that will show you, bluntly, that your tiles rebuilt 40 times during one scroll.\u003C\u002Fp>\n\n\u003Cp>Tune \u003Ccode>cacheExtent\u003C\u002Fcode> only after you have measured. The default (250 logical pixels beyond the viewport) is fine for most lists. Cranking it up hides build cost during fast flings, but building further ahead is not free either.\u003C\u002Fp>\n\n\u003Cp>My whole routine now fits in four lines, and I keep it pinned as a snippet in \u003Ca href=\"\u002Fsnippetark\u002F\" rel=\"noopener noreferrer\" target=\"_blank\">Snippet Ark\u003C\u002Fa>: builder constructor, itemExtent where heights match, no shrinkWrap on long lists, DevTools before and after. The budget Android scrolls at 60fps now. The tester closed the bug with the comment \"magic\". It was not magic. It was deleting work the list never needed to do in the first place.\u003C\u002Fp>\n",1789366676962]