Skip to content
Sailesh DahalAI Integration Consultant

I built the same infinite video feed in DartNative and Flutter

One ugly screen, built twice and measured

By Sailesh Dahal, AI Integration Consultant8 min readView as Markdown

I spent years shipping Flutter apps, and two of them had infinite video feeds. That screen never stopped annoying me. video_player was slower to start a clip than I wanted, it caches nothing on its own, and a feed puts both of those in front of the user on every swipe. My fix at the time was to push everything through Mux for HLS and pull in community packages the Flutter team does not maintain. Having to leave the official ecosystem to make a core feature feel acceptable always struck me as a bad sign.

I was not the only one hitting this. flutter/flutter#62265, open since 2020, is someone else asking how to build exactly this screen after PageView and ListView both gave them lag and crashes on long scrolls. flutter/flutter#28094 has been asking for video_player to cache anything at all since 2019, with 140 thumbs up and still open.

So when I reviewed DartNative I finished with an instruction to myself. Give it one ugly screen, not a whole app. This post is me doing that. Both apps, the raw logs and the screen recordings live in video-feed-showdown, and every number below comes from RESULTS.md in that repo.

Mux is only the history that made me curious. Neither app uses it, and both stream the same plain Pexels MP4s with nothing transcoding in front of them.

Building the thing

Both apps read the same generated config: the same 13 Pexels clips, 1000 rows, three video players alive at a time. Swipes came through argent as slow, momentum-free drags, so every gesture is the same deterministic pull rather than a fling of unknown speed. Both apps write to the same event log, byte identical on each side.

I ran them on a cabled iPhone 16 Pro Max in profile mode and on an iPhone 17 Pro Max simulator in debug. Those two sets of numbers are not interchangeable, so I keep them apart below.

DartNative has no PageView, and you cannot write one in Dart

This was the first real cost, and it showed up before any measuring did. A vertical feed needs one flick to move exactly one page. DartNative does not ship that. Its widget reference lists PageView as not planned, its skill guide suggests tabs with an IndexedStack or a horizontal list instead, and its own playground carousel says in the header that it was built by hand because there is no paging API.

You cannot fake it from Dart either. FastListController only moves by index, and for a row exactly one screen tall every alignment option resolves to the same offset, so there is nothing there to drive a finger-following pager with.

What did work was 22 lines of Objective-C that switch on UIScrollView.pagingEnabled on the native list, called from Dart through DynamicLibrary.process(). Paging kicks in about 2ms after the first frame.

Two things bit me on the way there. The first is that a safe-area inset breaks the alignment. Paging steps by exactly the view height, so any inset leaves every page short and a sliver of the previous video stays on screen. Zeroing the inset fixes it. The second is stranger. Paging snaps fine at 40 rows and at 1000 rows, then silently stops snapping at 10000, where the list just rests wherever your finger left it. That is why both apps are pinned to 1000 items, and it is a nasty surprise for anyone who assumes an infinite feed means an unbounded list.

So yes, PageView-style interaction is achievable. It costs a native plugin and two gotchas that are not written down anywhere.

The load gap is visible on screen

The load test is eight rapid swipes 500ms apart, deliberately faster than the preload window, run on the simulator in the same session for both apps. Here is where each one ended up: DartNative on the left, playing full bleed with no buffering spinner, Flutter on the right, with a spinner over the clip.

Side by side recording of the same eight rapid swipes on each app. DartNative on the left keeps playing with no stall. Flutter on the right shows a buffering spinner appear mid-clip.

Full-length, unclipped source recordings for both apps are in the repro folder of the repo, along with the same swipe test on a gentler five-swipe pass.

In that run DartNative’s time to first frame had a median of 62ms. Flutter’s was 1207ms, and one row hit a genuine network timeout. There were zero errors in every DartNative run in RESULTS.md. That is the screen I was complaining about at the top of this post, reproduced on demand.

I do not think it means DartNative decodes video faster, though, and here is why.

Mostly it is caching. DartNative’s video player has byte-range pre-caching built in and a 100MB disk cache. video_player has neither, which is the same gap flutter/flutter#28094 has been open about since 2019. On iOS, video_player has also been reported to download the whole file before it starts playing rather than streaming it (flutter/flutter#126760), which is its own reason a swipe-to-play feed feels slow. Both apps in this comparison hit the same URLs, so the gap here is about what the two players come with, not about the rendering pipeline.

The rest of it is that the two apps did not do the same amount of work. Row 8 against row 32 is the whole story of those two screenshots. Native paging is locked to one page per swipe, so DartNative visited fewer rows and every one it landed on was already warm. Flutter’s PageView physics carry the same gesture much further, so it kept landing on rows the three-player pool never had a chance to touch. PageView has no preload of its own either (flutter/flutter#31191, open since 2019), so landing on a cold row is the expected outcome, not a surprise. The spinner is that, not a defect. A gentler five-swipe test showed the same pattern, DartNative on row 5 and Flutter on row 14.

And these load numbers are debug builds on a simulator, which RESULTS.md flags as not representative of real performance. In that same simulator session DartNative was the slower one to start, at 1371ms against Flutter’s 548ms. Both apps have a code path to bundle the videos as local assets and take the network out entirely, which is the test that would separate rendering from caching. I never bundled them, so that number does not exist yet.

The measurement I wanted most is the one I could not take

On the physical iPhone in profile mode, DartNative started in 863ms and its time to first frame had a median of 88ms across 48 rows, with zero errors.

The Flutter column is empty. Four flutter run --profile attempts against that phone each hung at “Installing and launching” for eight to ten minutes and never found a VM Service. No crash report, and the app process was alive on the device the whole time. Launching it directly with devicectl worked once, but flutter attach still could not see it. DartNative needed one retry on its first launch against the same device, then ran every time after that.

I read that as a Flutter and CoreDevice tooling problem on this particular phone rather than anything about Flutter’s runtime. Either way the consequence stands. 863ms and 88ms have nothing to compare against, and the on-device half of this comparison is a single column.

What I would do with it

Plenty is missing. I did not capture memory or dropped frames. That is the obvious next step, and the launch problem above blocks it. I measured binary size for DartNative only, 26.1MB for a profile build, with no Flutter build to put beside it. And the network-free test above never happened.

If your hard screen is a media feed, this is a fair thing to try, on one condition. You should already have given up on video_player on its own. What I measured is mostly that DartNative’s player arrives with the caching layer I used to bolt on by hand.

If your hard screen is a long interactive list that is not a pager, the paging findings do not apply to you. If it is a pager, budget for the native shim, the inset fix and the item ceiling before you decide.

And if you take one thing from this, take the setup rather than the numbers. Two apps from one config, one shared logger, the same deterministic swipes and the raw logs kept in the repo are the reason I can say which part of the gap is the framework and which part is a cache. A demo video cannot do that, and neither can a benchmark you cannot re-run.

Sources

Primary:

Every number here is parsed from a raw event log kept in the repository, and the parser command is in the README. The physical-device column covers DartNative only. The simulator numbers are debug builds and are directional at best, and the caching asymmetry between the two video players is not controlled for anywhere in this pass.

The pain points above are not unique to me. These are open issues on flutter/flutter, linked where they back up a specific claim in the post above: