I make a media player for Android TV boxes, phones and Windows. People sign in with the playlist their provider gives them, and the app downloads three lists from the provider's server: live channels, films and series. Each one is a single JSON array.

On paper that is easy: await (await fetch(url)).json(). In practice it broke on real hardware, and the fix taught me more about memory on cheap devices than anything else this year.

How big these lists really are

These are measured numbers from real accounts, not guesses:

  • one list alone: 72 MB of JSON
  • the three lists of one account together: 156 MB
  • another account: 28,445 channels, 157,163 films and 50,149 series, 131 MB in total

Reading one of those whole needs several times its size in memory at the same moment: the raw bytes, the string made from them, and every object JSON.parse builds. On an Android TV box with 4 GB of RAM the download simply failed. The fetch threw "Network request failed", our app showed "No internet connection", and the playlist stayed empty. The user's internet was fine. The box had run out of room.

Rule one: never hold the list

The fix was to stop treating the response as one value.

  1. Stream the body. The list is read as the bytes arrive, a piece at a time.
  2. Cut the array, don't parse it. A small splitter walks only the outline of the array: brackets, braces and strings. When it has one complete item it hands that item's text to JSON.parse on its own. A 72 MB list becomes tens of thousands of tiny parses.
  3. Back-pressure. The splitter hands items over 2,000 at a time and reads no further until the database has taken them. Memory stays flat no matter how long the list is.
  4. Save in fixed blocks. Rows go into SQLite through one prepared INSERT of 2,900 rows that is built once and reused. Leftover rows wait for the next batch, so every statement is the same shape.

The trap: backslashes

The first version of the splitter was fast on one server and painfully slow on another, with lists of the same size.

PHP writes every / as \/, so lists from PHP panels are full of escapes. The splitter looked for the next backslash inside each string to know whether a quote was escaped. On a list written as plain JSON there are no backslashes at all, so every search ran to the end of the piece. In Node, 150,000 films (50 MB) took 0.15 s in PHP's format and 6 s as plain JSON, and minutes when the pieces were 1 MB.

The fix: remember where the next backslash is and search again only after you pass it. Same for the next quote. Now both formats take about 0.1 s, and the time grows only with the size of the list, not with what is inside it.

A related case: PHP writes every non-ASCII letter as \uXXXX. 20,000 test series with Arabic plots (72 MB) took 3.6 s before that change and 0.5 s after.

On Android: a file beats the network stream

Streaming fixed the crashes, but the box was still slow. The timers showed the cost was turning many small network pieces (8 KB each) into text on the JavaScript thread.

So on Android the list now goes to a file in the cache folder first, written by native code, and is read back one megabyte at a time. A megabyte is in memory, never the list. Text that arrives in large runs is also text the engine parses quickly: JSON.parse of a 78 MB film list took 0.55 s in megabyte runs, against 4 s when the same runs were put together from 8 KB pieces.

On one list of 200,000 films that meant about 4 s to read back, parse, build rows and save, where the same work took about 12 s before. If the device has less than 300 MB free, or the file cannot be written, the app falls back to reading the list as it arrives.

What I would tell anyone loading big JSON on a TV box

  • Measure on the device. My PC was never the problem.
  • Treat "Network request failed" with suspicion on low-memory hardware. It may be memory, not network.
  • Never response.json() something whose size you don't control.
  • Keep batches a fixed size so statements can be prepared once.
  • Test with the ugliest real data you can get: escaped slashes, \u escapes, huge single items.

If you want to see the result, the app is NewTV Player. It is a player only, with no content of its own: you add the playlist from your provider. I'm its developer, so take the plug for what it is; the techniques above work in any React Native app.

This post was drafted by an AI assistant from my own project notes and measurements.