Production deployment

The checklist that turns "it works on my laptop" into "it ships". One page per concern; each concern is one paragraph + the code that matters.

Six production concerns turn a working grid into a shippable one: server-side data, virtualization, accessibility, SSR and hydration, CSP, and TypeScript.

Open the live example: Admin template (Getting Started)

1. Pin your versions

The examples on this page run against these rows:

<script lang="ts">
  import { SvGrid, type GridColumns } from '@svgrid/grid'

  type Person = {
    id: number
    name: string
    email: string
    department: string
    age: number
    salary: number
    city: string
    startDate: string
    active: boolean
  }

  const people: Person[] = [
    { id: 1, name: 'Ada Lovelace',   email: '[email protected]',   department: 'Engineering', age: 36, salary: 142000, city: 'London',   startDate: '2021-03-01', active: true },
    { id: 2, name: 'Grace Hopper',   email: '[email protected]', department: 'Engineering', age: 45, salary: 168000, city: 'New York', startDate: '2019-07-15', active: true },
    { id: 3, name: 'Linus Torvalds', email: '[email protected]', department: 'Platform',    age: 54, salary: 155000, city: 'Portland', startDate: '2020-01-20', active: false },
    { id: 4, name: 'Radia Perlman',  email: '[email protected]', department: 'Networking',  age: 49, salary: 161000, city: 'Seattle',  startDate: '2022-09-05', active: true },
    { id: 5, name: 'Barbara Liskov', email: '[email protected]', department: 'Platform',  age: 52, salary: 172000, city: 'Boston',   startDate: '2018-11-11', active: true },
  ]

  const columns: GridColumns<Person> = [
    { field: 'name',       header: 'Name',       width: 200 },
    { field: 'department', header: 'Department', width: 150 },
    { field: 'city',       header: 'City',       width: 140 },
    { field: 'age',        header: 'Age',        width: 90 },
    { field: 'salary',     header: 'Salary',     width: 130, format: { type: 'currency', currency: 'USD' } },
  ]
</script>
{
  "dependencies": {
    "@svgrid/grid": "1.0.0",
    "@svgrid/enterprise":       "1.0.0"
  }
}

Pre-1.0, prefer exact pins (no ^, no ~). The changelog annotates breaking changes; the API stability page names which exports are under the semver promise.

2. Bundle size: what actually ships

Measured gzipped, with Svelte excluded as a peer dependency:

What you import Gzipped Minified
Headless core (createGrid + createCoreRowModel) ~2 KB ~7 KB
Full <SvGrid> render component (everything) ~77 KB ~340 KB

Add 9 KB gzipped for the render component's CSS. Charts, date/time editors, menus, and export split into import() chunks (64 KB total) that load on demand. Re-measure with pnpm size.

The <SvGrid> component is batteries-included: virtualization, Excel-style filters, inline editing, grouping, tree, master/detail, and accessibility are all in that one import. For a smaller footprint, use the headless core and render your own markup, registering only the features you need.

Enterprise adds per feature you import:

Enterprise module Approx KB Peer deps
exportGrid (csv/tsv/html) ~6 KB -
+ xlsx ~6 KB jszip (loaded on first xlsx call)
+ pdf ~9 KB pdfmake (loaded on first pdf call)
importData ~7 KB jszip (xlsx only)
AI helpers ~3 KB -. You bring your provider.
createPivotModel ~4 KB -

Use the subpath imports to avoid pulling features you don't use:

import { exportGrid }      from '@svgrid/enterprise/export'    // export only
import { createPivotModel } from '@svgrid/enterprise/pivot'    // pivot only

3. Peer dependencies

Peer dep When you need it Install
svelte Always. SvGrid renders against Svelte 5. pnpm add svelte
jszip xlsx export OR xlsx import. pnpm add jszip
pdfmake PDF export. pnpm add pdfmake

Both jszip and pdfmake are dynamic imports - the bundle splits and loads them on the first call. Nothing ships in your initial chunk until the user actually clicks "Export to xlsx".

4. Lazy-load Enterprise at route boundaries

If only one route in your app needs export, gate installEnterprise behind a dynamic import so the rest of the app doesn't ship the Enterprise bundle:

<script lang="ts">
  import type { SvGridApi } from '@svgrid/grid'
  import type { EnterpriseGridApi } from '@svgrid/enterprise'

  let api = $state<SvGridApi<typeof features, Order> | null>(null)
  let pro = $state<EnterpriseGridApi<typeof features, Order> | null>(null)

  async function enablePro() {
    if (!api) return
    const { installEnterprise, setLicenseKey } = await import('@svgrid/enterprise')
    setLicenseKey(import.meta.env.VITE_SVPRO_KEY)
    pro = installEnterprise(api)
  }
</script>

<SvGrid {...} onApiReady={(next) => (api = next)} />

<button onclick={enablePro}>Enable export</button>
{#if pro}
  <button onclick={() => pro?.exportData({ format: 'xlsx' })}>⬇ XLSX</button>
{/if}

5. License the Enterprise pack

// main.ts (or +layout.svelte for SvelteKit)
import { setLicenseKey } from '@svgrid/enterprise'

if (import.meta.env.VITE_SVPRO_KEY) {
  setLicenseKey(import.meta.env.VITE_SVPRO_KEY)
}

Enterprise is soft-gated - it works unlicensed, but renders a small watermark + a one-time console nudge. Set the key once at app startup; both disappear.

Don't commit the key to source control. Inject via env (Vite reads VITE_* variables at build time; SvelteKit reads $env/static/public).

For per-tenant deployments where each tenant has their own key, set the key inside the consumer's bootstrap, never inside the library package.

6. CSP-safe deployment

The recommended Content-Security-Policy header:

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data:;
  font-src 'self' data:;
  connect-src 'self';
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';

No 'unsafe-eval', no 'unsafe-inline' on script-src. SvGrid Community + Enterprise run clean under this policy. Demo 16 includes a runtime self-check.

If you ship in an iframe (embedded analytics, dashboards), add frame-ancestors to the host's CSP to allow the embed.

7. SSR

For SvelteKit:

// +page.server.ts
export async function load() {
  const rows = await db.query('select * from people limit 100')
  return { rows }
}
<!-- +page.svelte -->
<script lang="ts">
  import { SvGrid, tableFeatures, rowSortingFeature } from '@svgrid/grid'
  let { data } = $props()
  const features = tableFeatures({ rowSortingFeature })
</script>

<SvGrid data={data.rows} columns={columns} features={features} />

The first paint contains the data in a real <table> (good for SEO + LCP). Hydration only attaches event listeners. See demo 19 for a sandboxed JS-disabled iframe that proves the markup is meaningful pre-hydration.

8. Performance budgets

Targets that have held up in production:

Surface Target What you do if you miss it
Time to first row visible < 200 ms Lazy-load Enterprise. Defer non-critical columns. Smaller initial page.
Scroll FPS (10k rows, virtualized) 60 FPS Cap overscan. Avoid cell render functions that allocate per render.
Sort over 100k rows < 60 ms Set editorType on numeric / date columns so sortFns.number / sortFns.date get used instead of sortFns.auto.
Filter input → re-render < 30 ms Debounce server-side filters; the local filter UI is already ≤ 16 ms for 10k rows.
Export 10k rows to xlsx < 1 s Don't include columns you're going to hide. Use columns: [...] to project.

The benchmarks page has the reproducible numbers.

9. Error boundaries

The render component throws on truly broken state (e.g. a field that doesn't exist on any row). Wrap in a Svelte error boundary or guard with if (rows.length === 0) for empty data. The grid's emptyMessage prop covers the empty case without crashing.

<svelte:boundary>
  <SvGrid {data} {columns} {features} />

  {#snippet failed(error, reset)}
    <div class="error">Grid failed: {error.message}</div>
    <button onclick={reset}>Retry</button>
  {/snippet}
</svelte:boundary>

10. Monitoring + observability

The grid emits everything you'd want to observe via callbacks: onSortingChange, onFiltersChange, onRowSelectionChange, onCellValueChange. Wire them into your analytics / logging:

function track(event: string, payload: object) {
  // sentry, posthog, your own beacon - pick one
}

<SvGrid
  ...
  onSortingChange={(s)     => track('grid.sort',   { clauses: s })}
  onFiltersChange={(f)     => track('grid.filter', { columns: f.columns.length })}
  onCellValueChange={(e)   => track('grid.edit',   { column: e.columnId })}
/>

A production-shaped grid

Everything a real screen tends to turn on at once: virtualization for the row count, a stable getRowId so selection survives a refetch, and the footer totals people ask for on day two.

<script lang="ts">
  import { SvGrid, type GridColumns } from '@svgrid/grid'

  type Order = { id: string; customer: string; status: string; total: number }

  // Enough rows that virtualization is doing real work.
  const orders: Order[] = Array.from({ length: 500 }, (_, i) => ({
    id: 'A-' + String(1000 + i),
    customer: ['Northwind', 'Contoso', 'Fabrikam', 'Adventure'][i % 4]!,
    status: ['pending', 'shipped', 'delivered'][i % 3]!,
    total: 80 + ((i * 37) % 900),
  }))

  const columns: GridColumns<Order> = [
    { field: 'id',       header: 'Ref',      width: 110 },
    { field: 'customer', header: 'Customer', width: 170 },
    { field: 'status',   header: 'Status',   width: 130 },
    { field: 'total',    header: 'Total',    width: 130, summary: 'sum',
      format: { type: 'currency', currency: 'USD' } },
  ]
</script>

<SvGrid
  data={orders}
  {columns}
  getRowId={(r) => r.id}
  sortable
  filterable
  summary
  pageable
  pageSize={25}
/>

See also

Live examples

  • Admin template - Self-contained admin app: sidebar + three pages (Dashboard, Orders w/ Enterprise export bar, Customers w/ inline edit). Read end-to-end in one file.
  • CSP-compliant grid - No eval, no inline scripts. Documented CSP header + live runtime self-check.
  • Server-side rendering - SvelteKit-style SSR with a sandboxed pre-hydration snapshot.