Minimizing the on-device storage footprint of your TV web app
Audience
Third-party developers building web apps (HTML5) for TV
Purpose
Principles and practices for minimizing package/asset size so the app takes little flash storage
Runtime
A Chromium-based web engine on the TV (Blink renderer + V8 JavaScript engine)
Terminology: Here, "flash" means the device's flash memory (storage) where the app is installed and stored. (It has nothing to do with Adobe Flash.)
Nature of this document: This is a best-practice guide. It is not intended to mandate compliance or to trigger any action based on compliance — use it as a reference for better app quality and user experience.
A TV's flash storage is limited and shared by the system and many apps. A large app package causes the following problems.
Installs/updates are slower and more likely to fail. The larger the size, the longer the download/write, and installs/updates can fail when storage is tight.
It pressures storage. When one app takes a lot of space, there's less room for other apps to install and less system cache space, degrading the whole device's usability.
It leads to loading cost. Larger code/assets increase parse/decode load, lengthening startup and affecting RAM usage too.
The problem doesn't end at install time. The cache/stored data the app creates while running occupies the same flash. Even if you make the package small, occupied space keeps growing if you don't manage this.
Conversely, keeping the app small makes installs/updates fast, reduces download traffic, frees storage, and improves startup performance.
Core principle:① Don't ship what you don't need · ② Ship what you do need in its smallest form · ③ Manage what accumulates after install · ④ Automate this in your build pipeline. If you rely on doing it by hand every time, something will always slip through.
This document is a companion to the Memory Optimization Guide, which covers RAM (runtime memory). Making assets smaller reduces not only storage but also the runtime memory spent on decoding/parsing.
2. What takes up space
First, understand what dominates your app package's size. Usually the order of magnitude is as follows.
File assets: sort your build output (dist) directory by file size and review the biggest first
Final package: record the package size for each release to keep a trend. This value directly determines the space it takes on the device.
Often a handful of large items account for most of the size. Before polishing many small ones, it's more efficient to handle the largest assets first.
3. Minimize bundle (code) size with build options
The JavaScript/CSS you load takes storage by itself. Most popular bundlers/frameworks provide size optimization by default in their production build, so simply turning the options on correctly yields a big win.
Option names and defaults change between tool versions. The config examples below are meant to convey the gist. When applying them for real, check the current version's options in the official docs linked at the end of each subsection.
3.1 Common principles (regardless of framework)
Always ship a production build. Development builds include warnings, debug code, and source maps, which are large.
Minify: JS with Terser/esbuild, CSS with cssnano/esbuild — strip whitespace/comments and shorten identifiers.
Tree-shaking (remove unused code): Works well only with ES modules (import/export). Choose libraries that support tree-shaking (ESM, sideEffects: false).
Remove unused CSS: Use PurgeCSS, or the purge/content settings of frameworks like Tailwind, to drop classes you don't use.
Exclude source maps from the package. Source map files are very large. Don't include them in the shipped package; store them separately if needed.
Dependency diet: Don't pull in a heavy library whole for one or two features; use only the parts you need, or consider a lighter alternative or your own implementation. Also remove duplicate dependencies that do the same thing.
Don't let multiple versions of the same library get bundled together. As the dependency tree deepens, it's common for two or three different versions of the same library to be included. Check for duplicate versions with npm ls <package>, and unify versions (dedupe, overrides/resolutions) so only one copy remains. (Verify with a bundle analyzer.)
Don't inline images as base64. base64 is about 33% larger than the raw bytes, and when mixed into a JS/CSS bundle it also lives in runtime memory as a string. Set the bundler's inline threshold small (or 0) and keep images as separate files. (webpack Rule.parser.dataUrlCondition.maxSize, Vite build.assetsInlineLimit)
Avoid excessive transpilation/polyfills. Lowering syntax the target platform already supports (down-leveling) needlessly grows the code. Set your build target to the target environment.
Set a size budget and enforce it in the build. Define a baseline for total output size so the build warns/fails when exceeded — this structurally blocks size regressions. (size-limit, bundlesize; Angular has built-in budgets — see 3.6)
Know precisely — the scope of code splitting: Splitting code per screen with dynamic import() benefits initial load time and runtime memory, but does not reduce the total amount of code bundled into the package. What reduces storage (total size) is tree-shaking, minify, removing dependencies, and removing unused CSS. Apply the two separately.
Images usually account for the largest share of an app package. The key is to not create images you don't need, keep only one copy of what you do need, size it to the minimum needed for display, use the most efficient format, and compress it as much as possible. The impact shrinks toward the bottom, so apply these from the top down.
4.1 First, check whether it needs to be an image at all
The smallest image is no image. The following need no file at all if drawn with CSS or code.
It's common to turn a design mockup into a file just because it arrived as an image. Go through your asset list and ask "can this be done with CSS?" for each one. Not only does size drop, but color/size changes become easier too.
4.2 Keep one copy of an asset and handle variants with code
Keeping color/direction/state variants of the same graphic as separate files multiplies the asset count. Handle variants with CSS.
Color variants → keep SVG with fill="currentColor" and control via CSS color. For raster icons, use mask-image + background-color, or filter.
Direction variants → don't keep left/right arrows as two files; use transform: scaleX(-1), and rotate(180deg) for up/down. RTL layout support works the same way.
State variants (normal / focus / selected) → instead of a per-state image set, change only color/size/opacity with CSS.
Size variants → a single SVG covers all sizes.
/* Handle color/direction variants with one icon copy */
.icon { color: #999; } /* SVG uses fill="currentColor" */
.icon:focus,
.icon.is-selected { color: #fff; }
.icon--flip { transform: scaleX(-1); } /* reuse the left arrow as a right arrow */
Turning 24 icons into files across 3 colors × 2 directions yields 144 files. Reusing the same asset keeps it at 24.
4.3 Resize to display size
Don't bundle a 4K original for an image shown at 200×300. Bundling a file pre-resized to the display size greatly reduces size.
If you need different assets per resolution (FHD/4K), prepare only the sizes you actually use.
4.4 Prefer lightweight formats
Photos / complex images → WebP recommended. Smaller than JPEG/PNG at equal quality. It's widely supported by TV web engines, making it the safest choice.
Even smaller alternative → AVIF. Higher compression, but it may not be supported depending on the web engine version. Always verify support on your target platform's Chromium version, and use WebP if you're unsure.
Logos / icons / simple shapes → SVG. Being vector, they're small and crisp regardless of resolution. Bundle multiple icons as an SVG sprite.
Animation → avoid GIF. Animated GIFs are large and use a lot of decode memory. Use CSS animations for simple UI motion (loading spinners, emphasis effects), and animated WebP when raster animation is truly required.
If you don't need transparency, use a format without an alpha channel (JPEG/lossy WebP).
4.5 Squeeze further with lossless/lossy compression tools
Even after choosing a format, re-compressing with a dedicated tool can reduce size further with no (or minimal) quality loss.
PNG (lossless):zopflipng (powerful but slow), oxipng/OptiPNG. If the color count is low, pngquant (palette-based lossy) saves a lot.
# Lossless PNG optimization (zopflipng)
zopflipng -m in.png out.png
# For PNGs with few colors, palette lossy compression is far more effective
pngquant --quality=65-85 --output out.png in.png
# JPEG → WebP (photos: lossy, quality 75-85 recommended)
cwebp -q 80 photo.jpg -o photo.webp
# Logo / transparent PNG → lossless WebP
cwebp -lossless logo.png -o logo.webp
# SVG optimization
svgo icon.svg -o icon.min.svg
Lossy vs lossless: For photos, lossy compression (WebP/JPEG around quality 75-85) gives the best quality-per-byte. Use lossless or a palette (pngquant) for logos, transparent images, and UI elements that must stay crisp. Always check quality by eye after compression and adjust the quality value.
4.6 Automate it in the build pipeline
Image optimization is easy to forget if done by hand each time. Handle it automatically at build time.
General purpose: the imagemin family of plugins (imagemin-webp, imagemin-pngquant, imagemin-zopfli, imagemin-mozjpeg, imagemin-svgo)
Node pipeline: sharp (fast resize + format conversion)
Bundler plugins: Vite/webpack image-optimization plugins that convert/compress automatically at build time
// Example: automate resize + WebP conversion with sharp (build script)
import sharp from 'sharp';
await sharp('poster_original.jpg')
.resize(400, 600) // resize to display size
.webp({ quality: 80 }) // lightweight format + lossy compression
.toFile('poster_400x600.webp');
Keep the originals separately. Optimization (resize/lossy compression) is irreversible, so keep the source originals in your repository/asset store and bundle the optimized versions only into the build output.
5. Fonts & other assets
5.1 Fonts
Bundle only the weights/styles you truly need. Limit to Regular/Bold and exclude unused Light, Thin, Italic.
Use subset fonts that contain only the characters you actually use. This is especially effective for CJK (Chinese/Japanese/Korean), where a full-glyph font can reach several MB.
Use WOFF2 — the best-compressed font format.
If the platform's built-in fonts are enough, not bundling a web font at all is the surest saving.
5.2 Local media
Minimize the count and length of media (intro videos, sound effects) bundled into the package, and compress with an appropriate codec/bitrate.
Where possible, don't bundle it locally — stream/download it at runtime to keep it out of the package size. But you must also have a cleanup policy so fetched files don't keep piling up on the device (Section 7).
5.3 Other data
Minimize large JSON/static data bundled in, and if it isn't needed on the first screen, move it out of the package via a runtime fetch. Here too, cap the response cache so it doesn't grow without bound (Section 7).
6. What not to bundle
What you didn't ship needs no optimization. No matter how much you shrink things with compression/resizing, nothing is a surer saving than leaving out features and files you don't need in the first place.
6.1 Re-examine the features and assets themselves
The question that should come before technical optimization is "is this really needed?" As releases pile up, unused things accumulate.
Low-usage features — if you have usage metrics, check them and remove or defer to the next release screens/features that are barely used. Dropping one feature often has more impact than compressing dozens of assets.
Large decorative assets — intro videos, splash animations, full-screen background illustrations. If it's a flourish users skip every time, it's not worth paying for in size.
Frame-image animations — especially expensive. First consider whether a CSS animation can give the same impression (4.1).
Experiment/event leftovers — A/B test variants, seasonal event assets, and pilot features are often left behind after they end.
This judgment is hard for a developer to make alone. It's more effective to have a step that reviews "what can be dropped this time" together with planning/design before release.
6.2 Things that commonly tag along
Files that got in by accident often take more space than the assets you added on purpose. Removing such files has no effect on app behavior, so it's the lowest-risk, surest saving.
Development source tree:node_modules/, .git/, build caches, dev config files
Source maps:.map files and the //# sourceMappingURL comments that point to them
Test/dev tools: test files and snapshots, Storybook, mock APIs/sample data, debug panels
Design originals: PSD/AI/Sketch files, un-optimized high-resolution original images — keep originals in the repository and bundle only the optimized versions.
Documents: README, changelogs, bundles of dependency license text (in the minimum form required if you have a notice obligation)
Unused assets: images, icons, fonts, and JSON no longer referenced after a UI refresh or plan change
Duplicate assets: the same image included several times under different names (comparing file hashes finds these easily)
Unused locales: translation files/region-specific assets for unsupported languages. If you support many languages, consider loading only the selected language at runtime.
6.3 Use an "include list," not an "exclude list"
Listing what to exclude misses things every time a new file appears. Structure it as explicitly specifying only what to bundle.
Configure the build so the output directory (dist) contains only the files needed to run, and package that directory as-is.
Add a step just before packaging that prints the file list and sizes for a visual check. Unexpected files are usually caught at this step.
# Show the 20 largest files in the output (pre-release check)
find dist -type f -printf '%s\t%p\n' | sort -rn | head -20
# Find duplicate files (same content hash)
find dist -type f -exec md5sum {} + | sort | uniq -w32 -d
Put this check into your release script, and record the total output size alongside it — combined with the size budget from 3.1, it prevents size regressions.
7. Managing storage that accumulates after install
Storage isn't fixed at install time. The cache/data the app creates as it runs occupies the same flash, and without a cap it keeps growing. It's not rare for an app with a small install size to occupy several times that a few months later.
7.1 What accumulates
Store
What accumulates
Management point
Cache API / Service Worker
Image/JS/API-response caches
Version the cache name; delete old-version caches
IndexedDB
Content lists, thumbnails, watch history
Cap item count/total; clean up old items
localStorage
Settings, tokens, small state values
Don't put large data/images in it
HTTP cache
Network responses
Control lifetime via cache headers
App data area
Downloaded media/thumbnails
Cap total downloads; provide a user delete option
7.2 How to do it
Set caps. Set limits on both count and total — e.g., "thumbnail cache: max 200 items / 20MB" — and when exceeded, evict the least recently used first (LRU).
Set lifetimes. Give re-fetchable data (content metadata, etc.) a TTL and delete it on expiry. Data kept "just in case it's needed" is the main cause of accumulation.
Delete old-version caches on app update. Put a version in the cache name and remove the previous cache once the new version activates. Skip this cleanup and one copy of the cache piles up with every update.
Check usage yourself. Read current usage with navigator.storage.estimate() and clean up on your own when a threshold is crossed. (Check support on your target web engine.)
Give users a way to delete. Offering "Clear cache" in the settings screen lets users short on storage resolve it without uninstalling the app.
// Remove old-version caches on app update (Service Worker)
const CACHE_NAME = 'app-v12'; // bump the version each release
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((names) =>
Promise.all(
names.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name)) // delete previous-version caches
)
)
);
});
// Check usage and clean up on your own
async function checkStorage() {
if (!navigator.storage?.estimate) return;
const { usage } = await navigator.storage.estimate();
if (usage > 30 * 1024 * 1024) { // e.g. over 30MB
await trimThumbnailCache(); // delete least recently used items first
}
}
If what you removed from the package just piles up in the cache instead, the saving disappears. The strategy of moving large JSON/media to a runtime fetch (5.2, 5.3) leads to real storage savings only when a cap and cleanup policy are in place too.
In closing
Storage (flash) optimization comes down to four points:
Don't ship what you don't need — start by clearing out unneeded features/decorative assets, remove unused code/CSS/dependencies/font weights, exclude dev files that tag along, and move large data/media to runtime.
Ship what you do need in its smallest form — don't make an image out of what CSS can draw, handle variants of the same asset with code, and use production builds (minify/tree-shaking), image resize/lightweight formats/compression, and font subsetting/WOFF2.
Manage what accumulates after install — put caps and lifetimes on cache/stored data, and clean up old-version caches on app update.
Automate in the build pipeline and measure — bake image optimization and bundle minimization into the build, and keep watching the bundle analyzer and final package size to prevent size regressions.
A small app installs and updates fast, saves storage, and even starts faster.
This document is a draft and will be continually improved through review. Refer to separate platform documentation for platform-specific policies and support details.
We use cookies to improve your experience on our website and to show you relevant
advertising. Manage you settings for our cookies below.
Essential Cookies
These cookies are essential as they enable you to move around the website. This
category cannot be disabled.
Company
Domain
Samsung Electronics
developer.samsung.com, .samsung.com
Analytical/Performance Cookies
These cookies collect information about how you use our website. for example which
pages you visit most often. All information these cookies collect is used to improve
how the website works.
Company
Domain
Samsung Electronics
.samsung.com
Functionality Cookies
These cookies allow our website to remember choices you make (such as your user name, language or the region your are in) and
tailor the website to provide enhanced features and content for you.
Company
Domain
Samsung Electronics
developer.samsung.com, google.account.samsung.com
Preferences Submitted
You have successfully updated your cookie preferences.