Code Splitting and Assets

How dynamic imports split the bundle, when a manual vendor group helps, and how imported assets differ from the public directory.

Syntaxbuild.rolldownOptions.output.codeSplitting = { groups: [{ name, test, priority? }] }

A single file for the whole application is slow on first load and invalidates completely whenever anything changes. Splitting breaks the output into chunks that load when needed and cache independently.

Automatic splitting

Every import() expression is a split point. Vite emits the imported module and its own dependencies as a separate chunk, fetched when that expression runs. A lazy route and defineAsyncComponent are both just this. Most applications need nothing more.

Manual groups

Vite 8 bundles with Rolldown, so manual grouping lives under build.rolldownOptions.output.codeSplitting. (build.rollupOptions is a deprecated alias of build.rolldownOptions.) codeSplitting takes groups, each with a name and a test matching module ids, plus optional priority and minSize. A vendor group holding dependencies that change rarely keeps that chunk cached across your own deploys. Keep groups few: grouping changes the order modules execute, which can break code with import side effects.

Imported assets

Importing an image or font gives you its final URL as a string. Files under build.assetsInlineLimit (4096 bytes) are inlined as a data URL instead; ?inline and ?no-inline force the decision. ?url is the explicit form of a plain asset import, and ?raw gives the file contents as text.

The public directory

public/ is copied to the root of dist unchanged. Reference those files by root-absolute path, such as /robots.txt; do not import them. They get no hash, so they cannot be cached indefinitely.

Example

Example · vue
<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue'
// A plain asset import; './hero.jpg?url' is the explicit longhand for it.
import heroUrl from '@/assets/hero.jpg'

const props = defineProps<{
  title: string
}>()

// import() makes this component its own chunk, fetched on first show.
const ReportChart = defineAsyncComponent(
  () => import('@/components/ReportChart.vue'),
)

const showChart = ref(false)
</script>

<template>
  <section>
    <h2>{{ props.title }}</h2>
    <img :src="heroUrl" alt="" width="640" height="320" />
    <button type="button" @click="showChart = true">Show chart</button>
    <report-chart v-if="showChart" />
  </section>
</template>

<style scoped>
img {
  max-width: 100%;
  height: auto;
}
</style>

When to use it

  • Keep a charting or editor library out of the initial download until the user opens that screen.
  • Stop visitors re-downloading framework code on every deploy by isolating a vendor chunk.
  • Decide whether an icon should be imported and hashed or placed in public/ under a fixed name.
  • Explain a large initial chunk by mapping each line of the build summary back to an import.

More examples

A vendor group for dependencies that rarely change

Higher priority matches first, so the framework packages land in vue-vendor and everything else from node_modules falls into vendor. minSize lets a group that stayed too small fall back to automatic chunking rather than emitting a tiny file. Reach for this when your dependency set is stable and your own code ships several times a week: visitors then re-download only the application chunk.

Example · typescript
// vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },
  },
  build: {
    // Vite 8 renamed rollupOptions to rolldownOptions.
    rolldownOptions: {
      output: {
        codeSplitting: {
          minSize: 20000,
          groups: [
            {
              name: 'vue-vendor',
              test: /node_modules\/(vue|vue-router|pinia)\//,
              priority: 20,
            },
            { name: 'vendor', test: /node_modules\//, priority: 10 },
          ],
        },
      },
    },
  },
})

Fonts and images in a scoped stylesheet

Three asset paths with three different outcomes in dist. The first two are processed: they are emitted into dist/assets with a content hash, or inlined when small. The third is left alone because it lives in public/. Use public/ only for files whose exact name matters to someone else, such as favicon.ico, robots.txt or a file a third-party script fetches by name.

Example · css
/* src/assets/fonts.css - imported once from main.ts */
@font-face {
  font-family: 'Inter';
  /* A relative URL in CSS is rewritten and hashed like an import. */
  src: url('./fonts/inter-variable.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-display: swap;
}

.card {
  /* Under 4096 bytes this becomes a data URL instead of a file. */
  background-image: url('./icons/chevron.svg');
}

.hero {
  /* public/backdrop.jpg: root-absolute, copied as-is, never hashed. */
  background-image: url('/backdrop.jpg');
}

What the chunks look like after splitting

favicon.ico and robots.txt sit at the root with their original names because they came from public/. ReportChart has its own chunk because it was reached through import(), so a visitor who never opens the chart never downloads those 38 kB. Compare this listing before and after a config change; it is the only reliable way to tell whether a group matched anything.

Example · bash
$ npm run build

vite v8.0.0 building for production...
✓ 164 modules transformed.
dist/favicon.ico                        4.29 kB
dist/robots.txt                         0.07 kB
dist/index.html                         0.48 kB | gzip:  0.31 kB
dist/assets/hero-Bq9f2mAa.jpg          84.10 kB
dist/assets/index-Cm7xKd1p.css          6.11 kB | gzip:  1.88 kB
dist/assets/ReportChart-DkS0pQv2.js    38.45 kB | gzip: 12.90 kB
dist/assets/vue-vendor-BhT4rZmq.js     92.72 kB | gzip: 35.41 kB
dist/assets/vendor-CqW8nLe3.js         51.30 kB | gzip: 18.04 kB
dist/assets/index-DpR5uYtk.js          27.66 kB | gzip:  9.75 kB
✓ built in 2.31s

Discussion

  • Be the first to comment on this lesson.