# If it runs on my 8 GB laptop, it runs on yours: making Better-IPTV feel as fast as it looks

> Better-IPTV 3.0 made the app look modern. Every release since has been about making it feel that way on a nine-year-old laptop: a guide that stalled at 64 channels, an mpv regression on Windows, boot migrations that did nothing on every start, and a loading spinner that is now gone.

- Author: mewset
- Published: 2026-10-18
- Tags: better-iptv, open-source, rust, tauri, performance, linux
- Canonical: https://githero.dev/blog/better-iptv-fast-as-it-looks

---

Better-IPTV is developed on an HP EliteBook from 2016. Two cores, four threads, Intel HD Graphics 620 and 7.4 GB of usable RAM. When I checked just now, 89% of that RAM was already in use: three Claude Code sessions and a Chromium window with X open. That's not a test setup. That's just a normal day. It's also the only rule I have for performance: if the app runs well on this machine, it'll run well on yours.

This post covers three weeks of Better-IPTV. Version 3.0.0 gave the app a completely new look. Everything after it, 3.1.0, 3.1.1 and 3.1.2, has been about making the app feel as fast as it looks. If you haven't read [how the project started](https://githero.dev/blog/building-better-iptv), the short version: a pull request that went nowhere, a Tauri app with Rust on the back and React on the front, and MPV doing the actual playback.

## The machine

Here's what I have to work with, straight from fastfetch:

-   **Laptop:** HP EliteBook x360 1030 G2
-   **CPU:** Intel Core i5-7200U, 2 cores and 4 threads, boost up to 3.1 GHz
-   **GPU:** Intel HD Graphics 620
-   **RAM:** 8 GB, of which 7.4 GB is usable
-   **OS:** Omarchy on Hyprland, Wayland

It wasn't always this machine. When the first version of Better-IPTV came out in November 2025, I was on a Dell Latitude 5520 with 32 GB of RAM. In January, the Dell had an accident involving a staircase. I popped its SSD out and into my old HP, and that's where Better-IPTV has been built ever since.

My IPTV playlist has 26,053 channels: 15,310 live channels plus movies and series. That's the data every measurement in this post was taken with. It's not a synthetic benchmark set, it's what I actually watch TV with.

A lot of developers test on fast machines and throttle the CPU in DevTools to simulate a slow user. At one point I started a benchmark with 4x CPU throttling, and halfway through I realised how completely idiotic that was. My laptop doesn't need to simulate a slow machine. It *is* the lowest spec Better-IPTV officially supports.

## 3.0.0: making it look modern

On September 23rd, [@orcdev](https://x.com/orcdev) reviewed Better-IPTV live on X. He went through the website and the app, and he questioned one thing in particular: the README said "Modern UI", and he didn't think it looked modern. He compared it to the dark, slightly glassy look of Netflix, Vercel and Resend. He was right. The blue-and-purple UI looked like an app from 2019. That was the moment I decided what 3.0.0 would be.

3.0.0 shipped on September 27th, 152 commits after 2.9.0. The parts that matter for this story:

-   **Design tokens, all the way.** About 590 hard-coded `gray-*` classes across 24 components were moved to tokens, one component per commit. The goal was that a theme change after 3.0 would be a change to the values in one CSS file and nothing else.
-   **Dark with an amber accent.** Background `#0F1013`, accent `#F2B441` with dark text on it. I measured every contrast ratio instead of judging by eye. The old white-on-blue buttons failed WCAG AA at 3.68:1. Dark text on the new amber is 9.98:1.
-   **A new layout.** The stacked header, search bar and tabs became a 72 px icon rail and a glass top bar. The now-playing bar became a floating dock over the grid. Settings went from a modal to a view.
-   **Posters that aren't cropped.** @orcdev pointed this one out. Movie and series posters now show at 2:3, the way they were made.
-   **New features.** A TV guide, TMDB posters and detail views, and a Home page with daily genre rows and a pick of the day.
-   **A new logo.** A play triangle cut into five stripes on an amber plate, picked from seven candidates because it was the only one that still read at 16 px. With the amber design, the old blue logo looked like it belonged to another product.

![Better-IPTV 2.9.0 Movies view: a blue and grey layout with tabs and category chips at the top, and a grid of cards where each poster is cropped to a wide strip above a title and a blue Play button.](https://githero.dev/blog/images/better-iptv/movies-2.9.0.webp)

*Movies in 2.9.0, taken the evening it was released.*

![Better-IPTV 3.0.0 Movies view: a dark layout with a sidebar of icons, the same category chips, a large Recently added banner with an amber Play button, and rows of uncropped 2:3 movie posters.](https://githero.dev/blog/images/better-iptv/movies-3.0.0.webp)

*The same view in 3.0.0. Same categories, posters at 2:3.*

Here's where I'd love to tell you the redesign was secretly a performance project. It wasn't. It actually *added* blur: `backdrop-filter` on the top bar, the dock and the modal backdrops. The one rule was never on list items, because a blur behind every card in a grid of thousands is how you set fire to an integrated GPU. The design doc also said to measure frame times with the glass on. I never did. I haven't noticed any difference on the HD 620 either, but "I haven't noticed" is not a measurement. I've thought about measuring it properly, but since I haven't seen it hurt anything yet, it sits on my kanban board under "maybe check someday".

What 3.0.0 did do was put a lot of new weight on the app. New features, new placeholders, a Home page that wanted data from everywhere at once. And the nine-year-old laptop showed me exactly where it was too heavy.

## 3.1.0: what the new look cost

Almost every hot spot I fixed in 3.1.0 was a feature that arrived in 3.0.0. The frontend numbers in this section were measured in headless Chromium with the real 26,049-channel playlist served through mocked IPC, on a React profiling build. Not the real app, but the real data.

### A TV guide that gave up at 64 channels

Channel cards show what's on right now. To get that data, the app walked the whole channel list and fetched guide data in batches. The walk only moved on when the cache changed, and once 100 channels in a row had no guide data, it never changed again. The walk stalled for good. On my list it stopped after seven calls: 64 of the 1,035 channels that have guide data actually got it.

Now only cards that are on screen fetch guide data. I checked 70 cards that should have data: before the fix, all 70 said "No guide data". After, none of them did.

The guide view itself was capped at the first 100 channels in 3.0.0. In 3.1.0 the cap is gone, and the rows are virtualized. It lists every channel that has programmes in the next five days, which is 1,069 rows on my list. Lifting the cap without that filter would have meant about 12,700 rows saying "No guide data". Opening the guide now renders 21 rows instead of 100, and layout went from 38 to 17 ms.

### 262 cards mounted, 60 visible

Switching from Movies to Live TV mounted 262 cards when only 60 fit on screen. Three separate bugs worked together here. The channel filter ran in a `useEffect` that wrote to the store, so the first render after a switch drew the old section's list with the new section's cards. The grid's column count was also set in an effect, so one frame was laid out with the wrong number of columns and every card remounted. And the scroll position was reset after the new list had already rendered.

All three now happen during render instead of after it. The result:

-   **Live TV → Movies:** 202 → 64 card mounts, React time 77 → 20 ms, and the two long tasks of 137 and 72 ms are gone.
-   **Movies → Live TV:** 262 → 60 card mounts, 31 → 7 ms.

### Seven divs for a missing logo

In 3.0.0, channels without a logo got a little test-pattern placeholder: seven coloured bars, each one its own element. It looked nice. It also turned out that 63% of my live channels don't have a logo. In 3.1.0 the bars are a single CSS gradient. A full grid at 1080p went from 1,464 to 1,044 DOM nodes, and layout time while scrolling dropped by about 30%.

The "Recently added" hero on Home had a similar problem. With the TMDB background scan running, it re-sorted the entire movie section (7,677 movies) every time a new poster came in, about 9 ms each time. Now it ranks its ten candidates once. A stream of 40 TMDB batches went from 364 to 22 ms of React time.

### Less data over the bridge

In a Tauri app, everything the Rust side sends to the UI is serialized to JSON and parsed again in the webview. My channel list was 9.66 MB of JSON, and a quarter of it was stream URLs. The UI doesn't need stream URLs to draw a card. Worse, for Xtream providers those URLs contain your username and password.

In 3.1.0 the channel list sends summaries without URLs, and playback goes by channel id. The Rust side looks up the URL and hands it to MPV itself. The JSON went from 9.66 to 5.30 MB, `JSON.parse` from 32 to 22 ms, and the webview no longer has your credentials or a way to make MPV open an arbitrary URL. One fix, two wins.

On the Rust side, the guide parser was running on a tokio worker thread. For a 430,000-programme XMLTV feed that took 1.3 to 2.4 seconds and blocked other async work while it ran. It now runs in `spawn_blocking` and decompresses gzip while it parses, instead of decompressing everything into memory first. Extra peak memory for that feed went from 431 to 207 MB. On a machine with 7.4 GB, of which a lot is already in use, 224 MB matters.

And one bug I only found because I added better logging. To make Xtream error messages clearer, I started logging the first bytes of failed responses. One of them started with `1f 8b`. That's the gzip magic number. reqwest had been built without gzip support, so when a provider sent compressed JSON, it went straight into the JSON parser as binary garbage.

## 3.1.1: it was never curl

3.1.1 was six commits, and one of them is my favourite bug of the year. On Windows, live channels took 10 to 13 seconds to start. On Linux, about one. My first theory was curl, and I wrote that into a commit before I had to correct myself.

Windows builds shipped with a nightly build of mpv. I bisected it down to a single mpv commit. My provider answers the first request with a 302 redirect, and since that commit, mpv resolved the HLS segment URLs against the address from *before* the redirect. The first segments came back 404, and the picture waited around nine seconds before mpv recovered.

I reported it as [mpv#18608](https://github.com/mpv-player/mpv/issues/18608). A maintainer opened [a fix](https://github.com/mpv-player/mpv/pull/18612) within a day, and it was merged on October 14th. Until that reaches a release, Better-IPTV on Windows is pinned to the official mpv 0.41.0. Live channels now start in about 1.2 seconds, and plain `.ts` streams in 0.7.

Along the way I also found `curl-enabled=no` in the `mpv.conf` on my son's Windows gaming desktop. I'd put it there myself during an earlier attempt to diagnose the problem, and then forgotten about it. That's the kind of thing that makes you trust your own measurements a little less.

## 3.1.2: a boot without a spinner

By 3.1.1 the app was fast once it was running. Starting it was still slow. A window, then a "Loading playlist…" spinner for a quarter of a second or more, then the UI. 3.1.2 is all about that first second and a half.

### Migrations that ran every single time

Better-IPTV keeps its data in SQLite. Three one-time migrations, from 2.8.0 and 3.0.0, were supposed to fix up the guide table once and then never run again. They ran on every start. Each one scanned the whole `epg_programs` table, found nothing to change, and moved on.

I'd been half blaming btrfs for slow first starts of the day. It wasn't btrfs. strace showed about 4,900 page reads on the main thread before the window appeared, all of them from those three scans. When the database wasn't in the page cache, the setup step alone took 465 ms. After the fix it takes 9 ms, and the window shows up after 351 ms instead of 792.

Then something odd happened. Loading the channel list got *slower* in the real app, from 63 to 92 ms. It took a while to see why: those useless scans had been reading the database into the page cache, so the channel query that ran after them found its pages warm. Remove the scans and the query pays for its own disk reads. The boot as a whole was still about 440 ms faster to the window. A real speed-up can show up as a regression if you only look at one number.

### Logs that were never written

To time the boot from the frontend side, I added log marks in the UI. None of them showed up in the log file. It turned out the app had never had permission to write frontend logs at all. Every log call from the UI had been silently rejected since the beginning. My real log file had 0 frontend lines out of about 3,400. That's fixed now, and it was only found because I needed the logs for performance work.

### No more spinner

The spinner existed because the whole UI waited for the full channel list before it mounted. 3.0.0 had already split the app into sections, Home, Live TV, Movies, Series, and that turned out to be what made the fix possible. The boot now picks its starting section before the first paint. The UI mounts right away, and each section gets its own placeholders and a retry button while its data loads. Live channels load first, the library right after. Home carries its own data and no longer waits for the channel list at all.

So the redesign did help performance in the end, just not the way I expected. It didn't make anything lighter. It gave the app a structure where every section can load and fail on its own.

## The numbers

Here are three versions side by side: 3.0.0, 3.1.1 and the new 3.1.2. 60 starts in total, measured on the laptop described above in the state it's normally in. When the run started, `free -h` said 4.9 GiB used, 2.6 GiB available and 5.6 GiB already in swap. Not a cleaned-out test machine, just my desktop with everything else still open.

Each version is a local release build with the same LTO settings users get, running against my real data with 26,053 channels and the network switched off, so the provider couldn't affect the results. Times are medians of five starts, in milliseconds from process start.

"Not cached" means the database file wasn't in the page cache, like the first start of the day. The binary and WebKit were already loaded, so this isn't a true cold boot, and I won't call it one.

3.0.0

3.1.1

3.1.2

3.0.0 → 3.1.2

Window visible, not cached

707 ms

775 ms

354 ms

−50%

"Loading playlist…" spinner

~400 ms

270–410 ms

none

gone

Home painted, cached

1,750 ms

1,656 ms

1,114 ms

−36%

Home painted, not cached

2,050 ms

2,378 ms

1,189 ms

−42%

Live TV with channels, cached

1,332 ms

1,277 ms

1,170 ms

−12%

Live TV with channels, not cached

1,728 ms

1,577 ms

1,135 ms

−34%

CPU time at boot (Home, cached)

3.19 s

3.10 s

2.40 s

−25%

Memory (RSS, Home)

606 MB

582 MB

525 MB

−81 MB

Look at the 3.1.1 column first. Compared to 3.0.0, it barely moves. That's expected: 3.1 was about what happens once the app is running, scrolling, switching sections, opening the guide. None of that shows up in a boot benchmark. The boot work is all in 3.1.2, and it's where almost every row drops by a third or more.

One number needs a caveat. 3.1.1's Home without a cached database spread from 1,767 to 4,153 ms across its five starts. With 5.6 GiB in swap, the machine was very likely swapping during those runs. In an earlier run the same afternoon, the same cell came in at 2,089 ms. 3.0.0 and 3.1.2 gave stable numbers in both runs, so 3.0.0 → 3.1.2 is the comparison I trust most.

That earlier run also shows where the time came from. It compared 3.1.1 with main before and after the spinner work. The migration fixes did most of it: Home without a cached database went from 2,089 to 1,272 ms on those alone. Removing the spinner took off another 100 to 140 ms on Home, and came out about even on Live TV.

The number I care about most isn't in a column, though. After testing the build without the spinner, I wrote down that Better-IPTV has never felt this good. Live TV opens before I've finished reaching for the mouse.

## What's still slow

It would be easy to stop there. Here's what I know is still left:

-   **150 to 200 ms** from the live channel list arriving to the first frame of the Live grid. That's the next job, and it needs a proper profile before I guess at a fix.
-   **Moving 5.3 MB of channel data** from Rust to the webview costs 130 to 156 ms. I had estimated about 30. A column-based format would bring it to about 2.3 MB. I've worked out the sizes but haven't built it yet, on purpose: I want to fix the render side first so I know which one is actually in the way.
-   **About 0.9 s of CPU at boot** that I can't explain yet, and a trickle of CPU activity for a few seconds after boot. It could be repaints, it could be garbage collection. I don't know.
-   **The glass.** Still not measured.

## Three weeks

3.0.0 was about looking like an app from 2026. Everything since has been about earning that look. None of the fixes in this post are clever. A walk that stalled, an effect that should have been a render, a migration that forgot it had already run, a redirect that mpv forgot. They were all found the same way: by using the app every day on a machine that doesn't forgive anything.

If it runs on my 8 GB laptop, it'll run on yours. Better-IPTV is free and open source, and it's on [GitHub](https://github.com/mewset/better-iptv).

* * *

3.0.0 was released on September 27th, 3.1.0 on October 11th, 3.1.1 on October 13th and 3.1.2 on October 18th, 2026. The 3.1.0 frontend numbers come from headless Chromium with mocked IPC and the real playlist. The boot numbers in the table come from 60 starts of local LTO release builds on my laptop on October 17th, median of five per cell, with 26,053 channels and no network. All three versions ran against today's database, which has a few additive schema changes from after 3.0.0. The Windows mpv numbers were measured on Windows against my own provider. Thanks to @orcdev for the review that started 3.0.
