Reels-style video feeds: the performance budget that matters.
A short-video feed feels fast when the next video is already prepared and the current player gets out of the way. It feels slow when every swipe starts from zero. The difference is a small number of engineering decisions about media, memory and when to load.
Flutter · Smart TV · Media · Kochi, Kerala · Remote worldwide
Start with a user-visible budget
“Make it fast” is not a specification. The useful budget is expressed in what the user experiences: time until the first frame, time until the next item can start, how long a spinner is acceptable, how much memory the feed may hold and how much data a session may consume. Those numbers depend on the audience and the device tier, so they should be agreed with the product owner rather than copied from a blog post.
I record the baseline on named devices and network profiles before changing anything. A measured before/after on the same setup is the only credible performance claim. It also stops the team from optimising a screen that was never the bottleneck.
Separate the feed from the media
A feed API should return lightweight metadata: id, media URL or asset key, poster, duration, author, caption, counts and the next cursor. It should not make the client download a full video just to render a card. The player resolves the media URL when the item becomes active or enters the prefetch window.
On the server, uploads need a processing state. A file is not playable the moment it arrives. Transcoding, thumbnails, duration and moderation status are asynchronous. The client should show a “processing” card rather than a broken player, and it should be able to refresh that state without a full feed reload.
class FeedItem {
final String id;
final String posterUrl;
final String? playbackUrl; // null while processing
final Duration? duration;
final FeedItem? next;
}
Use one active player, not one per card
The simplest implementation creates a video controller for every card. It also creates a memory problem: decoders, buffers and textures multiply as the user scrolls. A better model has one active player, a small prepare window and an explicit release path. The UI can show a poster for every card and attach the player only to the item that is visible.
When the active item changes, pause and release the old controller, cancel pending work, then prepare the new item. Do not keep a paused player alive “just in case” unless you have measured the cost and decided it is worth it on your lowest supported device.
void activate(FeedItem item) {
_active?.pause();
_active?.dispose();
_active = createController(item);
_prefetchQueue.replaceWith(item.next);
}
Prefetch a small, bounded window
Prefetching is a budget, not a setting. Loading the next two items can make a swipe feel instant; loading ten can exhaust memory and data before the user reaches them. The window should depend on device tier, network type and whether the user is on Wi-Fi or mobile data.
I also separate “prepare metadata and poster” from “prepare decoder”. The poster is cheap and can be cached broadly. The decoder is expensive and should be reserved for the item that is likely to play next. If the user scrolls quickly, the prefetch queue should be cancellable rather than racing to prepare items that are already off-screen.
Measure first frame, rebuffer and memory
Useful measurements are the ones you can repeat: time from item activation to first rendered frame, time spent buffering after playback starts, dropped frames during scroll, memory high-water mark, network bytes per session and the number of player initialisations. Log them with the device model, OS version and network profile attached.
| Signal | Why it matters | How to check |
|---|---|---|
| First frame | It is the moment the feed feels alive. | Timestamp from activation to first rendered frame. |
| Rebuffer ratio | Stalls break the scrolling rhythm. | Player events plus network profile. |
| Dropped frames | Jank makes the feed feel cheap. | Frame timing during scroll and playback. |
| Memory high-water mark | Low-end devices kill apps that hold too much. | Device profiler on the lowest supported hardware. |
| Initialisation count | Too many players means wasted work. | Count controller creation and disposal per session. |
A single average hides the experience. Look at the slowest devices and the worst network profile, because those are the users who abandon the feed.
Design the failure states
A video can fail because the network drops, the codec is unsupported, the file is still processing, the signed URL expires or the app loses audio focus. Each case needs a visible state and a next action. An endless spinner is not a failure state.
I use a poster with a clear retry button, keep the rest of the feed usable, and avoid auto-retrying in a loop that burns data. If a media URL is signed, refresh it before retrying. If the decoder fails on a specific device, fall back to a lower profile or show a clear message rather than crashing the feed.
Release checklist
- ✓Define the budget
First frame, rebuffer, memory and data use, measured on named devices. - ✓Keep the feed metadata light
Do not download media to render a card. - ✓One active player
Pause, dispose and cancel work when the active item changes. - ✓Bound the prefetch window
Make it cancellable and aware of network type and device tier. - ✓Show useful failure states
Poster, retry and a feed that keeps working. - ✓Test the lowest device
The flagship phone is not the product.
If you are building a short-video product, start with the pipeline and the player budget before polishing the feed animation. The animation is easy to change. A feed that stalls on the first scroll is much harder to recover from.