Asset Best Practices

Best practices for organizing, optimizing, and managing frontend assets in a Tauri v2 application using React and Vite

Assets in a Tauri app are the static files your frontend needs to work: images, fonts, icons, JSON data, and other media. They live inside your Vite project and get bundled into the final application binary or served locally by the webview. How you organize, name, and optimize these files directly affects startup time, binary size, and how easy the project is to maintain.

This guide covers the patterns that keep assets manageable and performant in a Tauri v2 + React + Vite stack. It assumes you already understand the basics of the public directory and how Vite handles imports.

How Tauri Serves Assets

When you run tauri dev or build for production, the webview loads your frontend from a local origin. During development, Vite’s dev server handles asset requests. In a production build, the compiled frontend is embedded into the Rust binary and served through Tauri’s built-in asset protocol.

Assets that are imported in JavaScript or CSS get processed by Vite’s build pipeline—hashed, optimized, and inlined if small enough. Assets placed in the public directory are copied as-is without hashing. Both end up served from the same origin, so there are no cross-origin restrictions.

Not the same as Resources:

Tauri’s Resources feature (configured in tauri.conf.json under bundle.resources) is for files you want to access from Rust code at runtime, such as sidecar binaries or configuration files. This guide focuses on frontend assets—files that live in your Vite source tree and are used by the React UI.

Organizing the Asset Directory

A flat assets folder with every file thrown into it becomes unreadable quickly. A predictable structure makes it obvious where to find and add files.

Start by grouping by type:

src/
└── assets/
    ├── icons/
    ├── images/
    ├── fonts/
    └── data/

For larger apps, you might add a second layer by feature:

src/
└── assets/
    ├── dashboard/
    │   ├── chart-placeholder.png
    │   └── widget-icons.svg
    └── settings/
        └── gear.png

The rule of thumb: if a file is used by a single component and nowhere else, you can co-locate it next to that component. But if it is reused or part of a shared theme, keep it in a central asset folder. This prevents accidental duplicates and makes global updates easier.

Duplicated Assets Bloat the Binary:

If you store the same company logo in three different component folders, Vite will treat each copy as a separate asset. That adds unnecessary bytes to your final binary and makes future brand updates error-prone.

Naming Conventions That Scale

Consistent naming makes assets searchable and self-documenting. For most projects, a few simple rules are enough:

  • Use kebab-case: logo-main.png, icon-delete.svg.
  • Include the dimension or resolution when multiple sizes exist: banner-1200x630.jpg, avatar-64x64.webp.
  • Be descriptive about the asset’s purpose, not its current role: icon-discard.svg is clearer than icon-red-x.svg.
  • No spaces, no special characters beyond hyphens and underscores. Spaces in filenames can break import paths or URL references.
# Good
icon-user-avatar-32x32.png
chart-revenue-q3.json
font-inter-regular.woff2
# Avoid
Image1.png
icon!!.svg
My_File_ 3 .jpg

Hashed Filenames Are Automatic:

Vite adds content hashes to imported assets during production builds (e.g., logo.8f3a2b.png). This means you don’t have to manually version file names. The original, human-readable name is only used during development, so it should still be meaningful for your team.

File Optimization Before Bundling

Every kilobyte in an asset file stays in your binary forever. Optimize before the build step, and let Vite’s pipeline handle the rest.

Images

Uncompressed PNGs and JPEGs are the most common source of unnecessary bloat. Convert to modern formats with good compression:

  • Use WebP or AVIF for raster images; both offer significantly smaller file sizes than PNG/JPEG at comparable quality.
  • Keep a PNG fallback only if you must support very old WebView2 environments (rare in modern Windows). Tauri’s minimum WebView2 version is evergreen, so WebP is safe.
  • For SVG icons, keep them as SVG—they are tiny and resolution-independent. Avoid converting SVG to PNG unless a specific integration requires it.

Tools to preprocess images before they land in your project: sharp (Node.js), Squoosh, or ImageOptim.

Fonts

Loading entire font families when you only need a subset of characters wastes space. Use subset fonts that contain only the glyphs your UI actually renders. Convert to woff2—it is the most compact web font format with universal browser support.

JSON and Static Data

If you ship a data.json with hundreds of records, consider minifying it and, if possible, splitting it so the UI loads only what it needs on demand. Large JSON files that are imported directly become part of a JavaScript module and get parsed at startup, which slows down the initial render.

Inlining Large Files Freezes the Main Thread:

Vite’s assetsInlineLimit (default 4 KB) converts assets smaller than the threshold into base64 data URLs embedded in the JavaScript bundle. If you raise that limit to inline a 200 KB image, the browser must parse and decode that data URL before the page becomes interactive. Keep the threshold low, or move large assets to an off-screen loading strategy.

Performance Patterns for Assets

Since Tauri’s webview loads from local storage, network latency is not a concern. But memory, CPU, and binary size still matter.

Lazy Load Non-Critical Images

Use the native loading="lazy" attribute for images below the fold. This defers decoding until the image approaches the viewport, lowering initial memory pressure.

// src/components/ProductGallery.tsx
export default function ProductGallery() {
  return (
    <section>
      <h2>Gallery</h2>
      <img
        src="/images/hero-product.webp"
        alt="Main product"
        loading="eager"
      />
      {Array.from({ length: 20 }, (_, i) => (
        <img
          key={i}
          src={`/images/product-shot-${i + 1}.webp`}
          alt={`Product angle ${i + 1}`}
          loading="lazy"
        />
      ))}
    </section>
  );
}

The first image loads eagerly because it is visible immediately. The remaining 20 wait until the user scrolls. In a desktop Tauri window that opens with a fixed viewport, this can cut the startup image decode cost by over 90% for galleries.

Avoid Bundling Everything

An asset that the user may never see doesn’t need to be inside the binary. If your app includes a “Help” section with dozens of tutorial screenshots, consider loading those images on demand from a local resource directory (using Tauri’s resource system) rather than importing them all upfront. That keeps the binary lean and the first paint fast.

Use Vite’s Build Plugins for Automated Compression

Add vite-plugin-imagemin to your vite.config.ts to losslessly compress images during the build. This ensures every image is as small as possible without manual intervention.

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import viteImagemin from "vite-plugin-imagemin";
export default defineConfig({
  plugins: [
    react(),
    viteImagemin({
      gifsicle: { optimizationLevel: 3 },
      optipng: { optimizationLevel: 5 },
      mozjpeg: { quality: 80 },
      webp: { quality: 80 },
    }),
  ],
  build: {
    assetsInlineLimit: 4096, // 4 KB, keep default
  },
});

This configuration compresses raster images at build time without changing your import statements. You write import logo from './assets/logo.png' as usual, and the output is optimized automatically.

Choosing Between src/assets and public

Vite offers two places to store static files, and the choice changes how the file is processed.

Place files in src/assets (or any folder under src) and import them in JavaScript or CSS. Vite will:

  • Copy the file to the output directory with a content hash in the filename.
  • Return a resolved URL when imported (import logo from './assets/logo.png' gives you a string like /assets/logo.8f3a2b.png).
  • Inline the file as base64 if it is smaller than assetsInlineLimit.

This is the right choice for most assets because it gives you automatic cache busting and dead-code elimination—if no import references a file, it won’t be bundled.

// Import style: asset gets hashed and cached correctly
import logo from "./assets/logo.png";
export default function Header() {
  return <img src={logo} alt="App logo" />;
}

Never Put Secrets in public:

Files under public are reachable by anyone who can open the webview’s developer tools. Environment variables, API keys, and internal configuration belong in Rust-side logic or Tauri’s resource system, never in a public-facing asset folder.

Caching and Cache Busting When the App Updates

When a user installs a newer version of your Tauri app, the webview may still hold cached copies of assets from the previous version, especially service worker caches or HTTP caches if you use them. Imported assets with content hashes automatically bust the cache because their filenames change when the content changes.

For assets in public that must keep a fixed filename (like config.json), you have two options:

  • Append a query parameter with a version number: /config.json?v=2.0.0. Update that version in your code with each release.
  • Use Tauri’s asset protocol scope to set Cache-Control headers. In tauri.conf.json, under app > security > assetProtocol, you can set scope to include cache directives, though this requires a careful setup.

For most use cases, relying on Vite’s hashing for imported assets and avoiding public files for frequently updated data is the simplest and most reliable strategy.

Security Considerations for Assets

By default, the webview’s origin can access all assets inside the frontend bundle. That’s intentional—your UI needs to display images and load fonts. However, this access also means that a dependency with a script injection vulnerability could exfiltrate the contents of any file served by the frontend origin.

Keep the following in mind:

  • Don’t bundle sensitive data like database dumps, private keys, or internal documentation as static JSON files in src/assets or public. Move that data to the Rust backend and expose it through Tauri commands with permission checks.
  • Review large data files that you import. If a 10 MB JSON database of user profiles accidentally ends up as an import, it becomes trivially readable through the devtools console in production.
  • Content Security Policy (CSP) headers can restrict which scripts run, but they cannot prevent reading files already served by the same origin. Defense in depth starts with not including sensitive files in the bundle.

Minified Source Code Is Still Readable:

Production builds minify JavaScript, but assets like JSON, SVG, and CSS are often left human-readable. Assume everything in the frontend bundle is visible to a determined user who opens the devtools. Design your data flow accordingly.

Example: A Complete Asset Setup

Here’s how a small Tauri project might put these practices together. The app uses a few icons, a brand font, and a hero image.

src/
├── assets/
│   ├── icons/
│   │   ├── icon-save.svg
│   │   └── icon-delete.svg
│   ├── images/
│   │   └── hero-banner.webp
│   └── fonts/
│       └── inter-latin-400.woff2
├── components/
│   └── HomePage.tsx
└── main.tsx

In main.tsx, the font is imported so Vite processes and hashes it:

// src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
import "./assets/fonts/inter-latin-400.woff2"; // triggers font bundling
ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

In HomePage.tsx, the hero image is imported and used with lazy loading:

// src/components/HomePage.tsx
import heroBanner from "../assets/images/hero-banner.webp";
import iconSave from "../assets/icons/icon-save.svg";
import iconDelete from "../assets/icons/icon-delete.svg";
export default function HomePage() {
  return (
    <main>
      <img
        src={heroBanner}
        alt="App dashboard preview"
        loading="eager"
        width={1200}
        height={630}
      />
      <nav>
        <button aria-label="Save">
          <img src={iconSave} alt="" />
        </button>
        <button aria-label="Delete">
          <img src={iconDelete} alt="" />
        </button>
      </nav>
    </main>
  );
}

The vite.config.ts enables image compression and keeps the inline limit at 4 KB:

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import viteImagemin from "vite-plugin-imagemin";
export default defineConfig({
  plugins: [
    react(),
    viteImagemin({
      webp: { quality: 75 },
      svgo: { plugins: [{ removeViewBox: false }] },
    }),
  ],
  build: {
    assetsInlineLimit: 4096,
  },
});

After running tauri build, the binary contains only optimized, hashed assets. The SVGs are tiny and may even be inlined. The WebP hero image is compressed. The font is subset and in woff2 format. Everything loads instantly because there’s no network round trip, and the initial download size is kept small.

Check Your Bundle Size:

Run npx vite build and inspect the dist folder. If any single asset is over a few hundred kilobytes, ask whether it needs to be loaded eagerly or included at all. Tools like rollup-plugin-visualizer help you see exactly what is taking up space.

Summary

Asset management in a Tauri app is about building a sustainable, performant frontend that ships as part of your binary. The decisions you make early—where to put files, how to name them, and what to optimize—stay with the project as it grows.

  • Organize assets by type, keep a central assets folder, and co-locate only when a file is truly single-use.
  • Use kebab-case, descriptive names, and include size information when multiple resolutions exist.
  • Compress images and fonts before they enter the repository; lean on Vite plugins to automate optimization at build time.
  • Prefer importing assets over the public directory unless a fixed URL is required.
  • Lazy load images that are not visible on the first paint, and never inline large files.
  • Assume all frontend assets are readable by the user; keep sensitive data in the Rust backend.