Skip to content
Blog | sirlisko | Luca Lischetti Blog | sirlisko | Luca Lischetti Blog | sirlisko | Luca Lischetti
Go back

Asset Format Strategy for React Native Apps

This post is part of A Year Building a React Native App with Expo, a retrospective series on the decisions and patterns that shaped a production React Native app.

Introduction

As a React Native app grows, so does its collection of images: icons, illustrations, photographs, animations. Without clear guidelines, developers make ad-hoc format choices: someone adds a PNG for an icon, someone else exports a complex illustration as SVG, another person drops a GIF where a Lottie would be half the size. Over time, the bundle bloats and the asset folder becomes inconsistent.

At work, we reached a point where our app had a mix of PNGs, JPEGs, SVGs, and the occasional GIF, with no consistent rationale for which format was used where. We needed to formalise the decision into something the whole team could follow without thinking too hard about it.

The result was a simple content-based selection strategy: SVG for vectors, WebP for rasters, Lottie for complex animations, compressed MP4 for video. That’s the short version. The rest of this post is the longer version: the reasoning, the tooling, and how it all fits together in a React Native app using Expo.

The Formats

WebP

WebP is a modern raster format that provides excellent compression compared to PNG and JPEG, often 25-50% smaller at equivalent visual quality. It supports both lossy and lossless compression, transparency, and even animation (as a GIF replacement).

Good for: photographs, photorealistic artwork, rasterised content, CMS-loaded images.

Not good for: anything that needs to scale across screen densities without quality loss. A WebP image at 2x looks fuzzy on a 3x display, just like any raster.

SVG

SVG is a vector format that scales infinitely without quality loss. A 24px icon and a full-screen illustration render from the same file with no degradation. When optimised with SVGO, SVGs are often smaller than their raster equivalents for non-photographic content.

Good for: icons, illustrations, UI elements, anything requiring dynamic styling (colour changes based on theme or state).

Not good for: photographs or photorealistic images. An SVG of a photograph would be enormous and defeat the purpose.

Lottie

Lottie renders After Effects animations exported as JSON. It’s purpose-built for complex, multi-step animations (loading spinners, onboarding sequences, micro-interactions) where frame-by-frame animation would be impractical as SVG or prohibitively large as video.

Good for: complex animations with timing, loading states, multi-step sequences.

Not good for: static images (overkill) or simple animations achievable with CSS/SVG transforms.

Compressed MP4

Video assets (onboarding sequences, background loops, explainer clips) are the heaviest files in any app bundle. Unlike images, there’s no alternative format debate: MP4 with H.264 is the universal choice. The question is how aggressively to compress.

We use ffmpeg with a standard command for all bundled videos:

ffmpeg -i input.mp4 -vf "scale=1080:-2" -crf 28 -an -movflags +faststart -y output.mp4

For videos used as backgrounds or behind masks/overlays, you can push compression further: lower resolution (scale=720:-2 or scale=480:-2), higher CRF (-crf 30 or -crf 32), and lower frame rate (-r 20). These videos are partially obscured anyway, so the quality loss is invisible.

Always verify on-device: if it looks too soft, dial back one setting at a time. And keep original source files outside the repo; this compression is lossy and irreversible.

Good for: onboarding sequences, background loops, explainer content, any motion content too complex for Lottie.

Not good for: content that needs to be pixel-perfect or where audio matters (consider streaming instead of bundling).

The Decision Matrix

This is the mental model the team follows:

Content TypeFormatReasoning
IconsSVGScales perfectly, small file size, themeable
IllustrationsSVGScales perfectly, optimise with SVGO
PhotographsWebPOnly viable option for photographic content
Rasterised artworkWebPContent that originated as raster
Complex animationsLottiePurpose-built for animation, small file size
Video contentCompressed MP4ffmpeg with CRF 28, faststart, no audio

If you’re unsure, ask yourself: “Is this a photograph?” If yes, WebP. “Does this need to animate with multiple steps or timing?” If yes, Lottie. “Is this real video footage or a long motion sequence?” If yes, compressed MP4. Everything else is probably SVG.

Implementation in Expo

Our app uses three libraries, each matched to a format:

SVG as Components

The react-native-svg-transformer is configured in metro.config.js so that SVG imports resolve as React components:

import BullseyeIcon from "@/assets/vectors/bullseye.svg";

<BullseyeIcon color="red" width={24} height={24} />

This means SVGs can be styled dynamically: you can change their colour based on theme, resize them via props, and treat them like any other component. No need for multiple sizes or colour variants of the same icon.

WebP with expo-image

For raster images, expo-image handles WebP natively with built-in caching, transitions, and lazy loading:

import { Image } from "expo-image";
import HERO_IMAGE from "@/assets/images/hero.webp";

<Image
  source={HERO_IMAGE}
  style={tw("h-64 w-full")}
  contentFit="cover"
  transition={300}
/>

Lottie Animations

Complex animations use lottie-react-native with JSON or .lottie files:

import LottieView from "lottie-react-native";
import animatedLogo from "@/assets/animations/animatedLogo.json";

<LottieView source={animatedLogo} autoPlay loop style={tw("h-[122px] w-full")} />

File Organisation

Assets are organised by format in a predictable structure:

src/assets/
├── animations/       # Lottie JSON / .lottie files
├── images/           # WebP raster images
├── vectors/          # SVG files (imported as components)
└── videos/           # Compressed MP4 files

This isn’t just cosmetic. It makes it immediately obvious where to put a new asset and what format it should be in. If you’re about to add a file to vectors/ and it’s a photograph, something is wrong. The folder structure itself enforces the strategy.

In our app, this breaks down to roughly 70 WebP images, 50 SVG vectors, and 12 Lottie animations, with only 6 legacy PNGs remaining that predate the strategy.

Converting Existing Assets

When we formalised this strategy, the app already had a mix of formats. Converting was straightforward with two CLI tools.

PNG/JPEG to WebP

ImageMagick handles raster conversion (I wrote about a bulk conversion function previously):

magick input.png -quality 80 -define webp:method=6 output.webp

For pixel-perfect output where lossy compression isn’t acceptable:

magick input.png -define webp:lossless=true output.webp

Optimising SVGs

SVGO strips unnecessary metadata, comments, and redundant attributes:

svgo input.svg

This is especially important for SVGs exported from Figma or Illustrator, which tend to include a lot of cruft that inflates file size without any visual impact.

Compressing Videos

ffmpeg handles video compression. The standard command for bundled assets:

ffmpeg -i input.mp4 -vf "scale=1080:-2" -crf 28 -an -movflags +faststart -y output.mp4

For background or decorative videos where quality is less critical:

ffmpeg -i input.mp4 -vf "scale=720:-2" -crf 32 -r 20 -an -movflags +faststart -y output.mp4

All three tools can be installed with Homebrew:

brew install imagemagick svgo ffmpeg

Why Not Just Use PNGs?

This is the question that usually comes up. PNG works everywhere, every tool supports it, and it’s “good enough”. The problem is that “good enough” compounds. A single PNG icon at 3x resolution might be 15KB where the SVG equivalent is 2KB. Multiply that across 50 icons and you’ve added 650KB to your bundle for no reason. And those PNGs can’t adapt to theme changes; you’d need separate light and dark variants, doubling the count.

WebP offers similar savings over PNG for photographs. In our experience, the compression ratios are consistently in the 40-60% range for photographic content, which adds up fast in an image-heavy app.

Conclusion

The strategy is deliberately simple: SVG for vectors, WebP for rasters, Lottie for complex animations, compressed MP4 for video. No exceptions, no “it depends”. The decision matrix fits on an index card and the folder structure enforces it by convention.

What made this work for our team wasn’t the technical details (those are straightforward) but having the strategy documented and agreed upon. Before we formalised it, every new image was a micro-decision. Now it’s mechanical: “What kind of content is this?” → look at the matrix → done.


Note: This strategy is specific to React Native with Expo, but the principles apply to any mobile or web project. The core insight (match the format to the content type, not the tool you happen to have open) is universal.



Previous Post
Accessibility in React Native: Beyond the Checklist
Next Post
Storybook in Expo Router Without Replacing Your App