Svelte Builds on GitHub Actions

Cache the package manager store, node_modules/.vite, and .svelte-kit so svelte-check and vite build share one restore, then size warp- runners per phase.

Last verified:

To build a Svelte or SvelteKit app on GitHub Actions, run svelte-kit sync and svelte-check in a type check job, run vite build in a build job, and let both jobs restore the same cache entries so neither one reinstalls dependencies or re-optimizes Vite's dependency graph. On WarpBuild runners the WarpBuilds/cache action is a drop-in replacement for actions/cache@v4 and stores those entries in WarpBuild's cache backend, which is enabled by default on Linux runners.

The rest of a Svelte pipeline splits into three phases with different resource shapes: a single-process type check, a Rollup production bundle, and a Vitest suite that scales across cores. This page covers the cache paths, a two-job workflow that shares them, per-phase sizing with rates applied, and the bottlenecks that dominate Svelte pipelines.

Overview

Every GitHub Actions job starts on a fresh virtual machine, so a Svelte repository has three directories worth restoring and one that belongs in an artifact rather than a cache.

PathWhat it holdsKey onReused by
$(pnpm store path) or ~/.npmPackage tarballs from the registryLockfile hashEvery job
node_modules/.viteVite's esbuild pre-bundled dependency chunksLockfile plus vite.config.tsType check, build, Vitest
.svelte-kitGenerated route types from svelte-kit sync plus build intermediatesLockfile plus svelte.config.js and vite.config.tsType check, build
build or the adapter output dirThe deployable outputNot cachedUploaded with actions/upload-artifact

The .svelte-kit directory is the piece that catches people moving from a plain Vite project. svelte-kit sync generates the $types modules that every +page.ts and +page.server.ts imports, and svelte-check reports those imports as errors when the directory is missing. Running sync once and carrying .svelte-kit into the build job removes a repeat of that generation step.

The adapter output stays out of the cache. A cache entry is keyed content that later runs restore, while the deployable bundle is a per-commit result that a deploy job consumes, which is what actions/upload-artifact and actions/download-artifact are for.

Svelte work belongs on Linux x64, and one constraint from the caching documentation makes that concrete: WarpBuild Cache is not supported on Windows runners, so keep the cached phases on Linux jobs.

Configuration

This workflow runs the type check and the build as separate jobs on warp-ubuntu-latest-x64-4x, with both restoring the same two cache entries.

name: svelte-ci
on:
  push:
    branches: [main]
  pull_request:

jobs:
  typecheck:
    runs-on: warp-ubuntu-latest-x64-4x
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: WarpBuilds/setup-node@v6
        with:
          node-version: 22
          cache: pnpm

      - run: pnpm install --frozen-lockfile

      - name: Restore Vite and SvelteKit state
        uses: WarpBuilds/cache@v1
        with:
          path: |
            node_modules/.vite
            .svelte-kit
          key: ${{ runner.os }}-svelte-${{ hashFiles('pnpm-lock.yaml', 'svelte.config.js', 'vite.config.ts') }}
          restore-keys: |
            ${{ runner.os }}-svelte-

      - run: pnpm exec svelte-kit sync
      - run: pnpm exec svelte-check --tsconfig ./tsconfig.json

  build:
    runs-on: warp-ubuntu-latest-x64-4x
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: WarpBuilds/setup-node@v6
        with:
          node-version: 22
          cache: pnpm

      - run: pnpm install --frozen-lockfile

      - name: Restore Vite and SvelteKit state
        uses: WarpBuilds/cache@v1
        with:
          path: |
            node_modules/.vite
            .svelte-kit
          key: ${{ runner.os }}-svelte-${{ hashFiles('pnpm-lock.yaml', 'svelte.config.js', 'vite.config.ts') }}
          restore-keys: |
            ${{ runner.os }}-svelte-

      - run: pnpm run build

      - uses: actions/upload-artifact@v4
        with:
          name: svelte-build
          path: build

Both jobs write the same key, so whichever finishes first saves the entry and the other restores it on the next run. The restore-keys prefix is what keeps a dependency bump cheap: when the exact key misses, the action restores the most recent entry with the same prefix, and Vite re-optimizes only the packages that changed.

WarpBuilds/setup-node handles the package manager store on its own. It is a drop-in replacement for actions/setup-node whose cache input accepts npm, yarn, or pnpm, and cache-dependency-path points the key at a lockfile outside the repository root. The full input list is in the setup actions documentation.

Two cache behaviors shape what you see in the logs. Entries are scoped to key, version, and branch, so seed the cache on main and let branch builds reach it through restore-keys. Entries expire 7 days after last use, so an active repository stays warm and an abandoned branch stops costing storage on its own. Cache storage is metered at $0.20 per GB-month with each write or restore operation at $0.0001, so a Svelte repository holding a 900MB store and running 3,000 operations a month adds about $0.48 to the bill.

Sizing

The Linux x64 sizes in the cloud runner catalog that matter for Svelte work:

Runner labelvCPUMemoryStoragePrice per minute
warp-ubuntu-latest-x64-2x28GB150GB SSD$0.004
warp-ubuntu-latest-x64-4x416GB150GB SSD$0.008
warp-ubuntu-latest-x64-8x832GB150GB SSD$0.016
warp-ubuntu-latest-x64-16x1664GB150GB SSD$0.032

Bundling. A production vite build walks the module graph in Rollup on one thread, then parallelizes minification across workers. The single-threaded graph walk sets the floor, so the curve flattens past 4 vCPU for most applications and past 8 vCPU for large route trees. Memory is the constraint that bites first: a SvelteKit app with hundreds of routes and heavy prerendering can push Node past the default heap, and 16GB on 4x covers that before NODE_OPTIONS=--max-old-space-size becomes a workaround on a machine that cannot back it. The dependency pre-bundle phase is the part that does scale, since Vite runs it through esbuild, and a warm node_modules/.vite skips it entirely.

Test suite. Vitest sizes its worker pool from the core count, so a suite with many independent component test files keeps 8 cores busy and earns 8x at $0.016 per minute. Component tests that mount into jsdom or happy-dom are memory hungry per worker, which is the other reason 8x with 32GB beats 16x with a worker pool that starves. A Playwright end-to-end job is different again: workers are bounded by browser instances rather than by test files, so 4x with a workers: 4 setting is usually the right shape.

Type check. svelte-check runs as one process over the whole program, so extra vCPUs do nothing for it. Keep it on 4x and give it memory rather than cores.

GitHub publishes its own per-minute list prices on the GitHub Actions minute multipliers reference. Checked on 2026-08-13, the 4-core Linux larger runner is $0.012 per minute. A worked model for a Svelte application running 1,500 pull request jobs a month, averaging 4 minutes each on a 4 vCPU machine with a warm cache, which is 6,000 runner minutes:

ScenarioRateMonthly minutesMonthly cost
GitHub-hosted Linux larger runner, 4 vCPU$0.012/min6,000$72.00
warp-ubuntu-latest-x64-4x, 4 vCPU$0.008/min6,000$48.00
Cache storage and operations (900MB, 3,000 ops)see aboven/a$0.48

Stated as list-price arithmetic: warp-ubuntu-latest-x64-4x (4 vCPU, 16 GB) costs $0.008 per minute against $0.012 per minute for the 4-core Linux larger runner (4 vCPU, 16 GB): 33 percent lower list price. GitHub list price checked on 2026-08-13.

Full rates for every size and platform are on the pricing page.

Bottlenecks

Cold dependency installs. Without a restored store, every job pulls the full dependency tree from the registry before Vite starts. The fix is the setup-node cache input above; verify it from the install log, where a warm pnpm install reports most packages as reused from the store.

Vite dependency re-optimization. With node_modules/.vite empty, Vite pre-bundles every CommonJS and deep-import dependency through esbuild at the start of the run. That work repeats in the type check job, the build job, and every Vitest shard. One shared cache entry removes three copies of it.

Missing svelte-kit sync. A type check job that runs svelte-check without sync fails on $types imports that no file generated yet, and the error text points at application code rather than at the missing step. Run sync as its own step so the failure is unambiguous.

Playwright browser downloads. A SvelteKit end-to-end job downloads browser binaries on every cold run. Cache ~/.cache/ms-playwright keyed on the Playwright version from the lockfile rather than on the lockfile hash alone, so a routine dependency bump does not throw away several hundred megabytes of browsers.

Single-shard test suites. A Vitest suite that runs as one job has a wall time equal to its total test time whatever the runner size. Vitest accepts a --shard flag, so the suite splits across a matrix where each shard restores the same cache entries and the wall time drops to the slowest shard.

Observability separates a CPU-bound Vitest phase from an install stuck on the network, and the Action Debugger pauses a workflow and opens an SSH session on the runner so you can inspect node_modules/.vite and .svelte-kit on the machine itself.

Proof

Public OSS repositories running warp- labels are citable evidence, and the check takes one click: open the workflow file and read the runs-on line. The Trigger.dev end-to-end suite runs its matrix on warp-ubuntu-latest-x64-4x and warp-windows-latest-x64-8x in triggerdotdev/trigger.dev's e2e.yml, checked on 2026-08-13. That repository is a TypeScript monorepo rather than a Svelte application, and the toolchain shape is the same one this page describes: a pnpm install, a Vite-era build step, and a test matrix on 4 vCPU Linux runners.

Moving a Svelte workflow over is a one-line change per job to runs-on. Rates for every size are on the pricing page.

The same cache-then-size approach applies across the JavaScript toolchain. The Node.js on GitHub Actions page covers package manager store caching in depth, Vue on GitHub Actions covers the same Vite phases with a different compiler front end, and Jest cache configuration covers the transform cache when a Svelte repository still runs Jest.

FAQ

Do I have to run svelte-kit sync before svelte-check in GitHub Actions?

Yes for a SvelteKit project. svelte-kit sync writes the generated route types under .svelte-kit/types, and svelte-check reports missing $types imports as errors when that directory is absent. A plain Svelte library with no SvelteKit router does not need the sync step.

What should the SvelteKit cache key hash?

Use two entries. Hash the lockfile for the package manager store, and hash the lockfile plus svelte.config.js and vite.config.ts for node_modules/.vite, since Vite re-optimizes dependencies when either config changes. Add a restore-keys prefix to both so a dependency bump restores the previous entry instead of starting cold.

What runner size does a SvelteKit build need?

warp-ubuntu-latest-x64-4x at $0.008 per minute covers svelte-check and vite build for most applications. Move the Vitest job to warp-ubuntu-latest-x64-8x when the suite has enough independent files to keep 8 workers busy, and leave the type check job on 4x since svelte-check runs in one process.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.