Merge branch 'main' into portals

portals
Simon Holthausen 3 months ago
commit 3321433bc1
No known key found for this signature in database

@ -0,0 +1,70 @@
---
name: performance-investigation
description: Investigate performance regressions and find opportunities for optimization
---
## Quick start
1. Start from a branch you want to measure (for example `foo`).
2. Run:
```sh
pnpm bench:compare main foo
```
If you pass one branch, `bench:compare` automatically compares it to `main`.
## Where outputs go
- Summary report: `benchmarking/compare/.results/report.txt`
- Raw benchmark numbers:
- `benchmarking/compare/.results/main.json`
- `benchmarking/compare/.results/<your-branch>.json`
- CPU profiles (per benchmark, per branch):
- `benchmarking/compare/.profiles/main/*.cpuprofile`
- `benchmarking/compare/.profiles/main/*.md`
- `benchmarking/compare/.profiles/<your-branch>/*.cpuprofile`
- `benchmarking/compare/.profiles/<your-branch>/*.md`
The `.md` files are generated summaries of the CPU profile and are usually the fastest way to inspect hotspots.
## Suggested investigation flow
1. Open `benchmarking/compare/.results/report.txt` and identify largest regressions first.
2. For each high-delta benchmark, compare:
- `benchmarking/compare/.profiles/main/<benchmark>.md`
- `benchmarking/compare/.profiles/<branch>/<benchmark>.md`
3. Look for changes in self/inclusive hotspot share in runtime internals (`runtime.js`, `reactivity/batch.js`, `reactivity/deriveds.js`, `reactivity/sources.js`).
4. Make one optimization change at a time, then re-run targeted benches before re-running full compare.
## Fast benchmark loops
Run only selected reactivity benchmarks by substring:
```sh
pnpm bench kairo_mux kairo_deep kairo_broad kairo_triangle
pnpm bench repeated_deps sbench_create_signals mol_owned
```
## Tests to run after perf changes
Runtime reactivity regressions are most likely in runes runtime tests:
```sh
pnpm test runtime-runes
```
## Helpful script
For quick cpuprofile hotspot deltas between two branches:
```sh
node benchmarking/compare/profile-diff.mjs kairo_mux_owned main foo
```
This prints top function sample-share deltas for the selected benchmark.
## Practical gotchas
- `bench:compare` checks out branches while running. Avoid uncommitted changes (or stash them) so branch switching is safe.
- Each `bench:compare` run rewrites `benchmarking/compare/.results` and `benchmarking/compare/.profiles`.

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: reject pending async deriveds on discard

@ -4,7 +4,6 @@ root = true
end_of_line = lf end_of_line = lf
insert_final_newline = true insert_final_newline = true
indent_style = tab indent_style = tab
indent_size = 2
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true

@ -0,0 +1,69 @@
name: Autofix Lint
on:
issue_comment:
types: [created]
workflow_dispatch:
permissions: {}
jobs:
autofix-lint:
permissions:
contents: write # to push the generated types commit
pull-requests: read # to resolve the PR head ref
# prevents this action from running on forks
if: |
github.repository == 'sveltejs/svelte' &&
(
github.event_name == 'workflow_dispatch' ||
(
github.event.issue.pull_request != null &&
github.event.comment.body == '/autofix' &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
)
)
runs-on: ubuntu-latest
steps:
- name: Get PR ref
if: github.event_name != 'workflow_dispatch'
id: pr
uses: actions/github-script@v8
with:
script: |
const { data: pull } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
if (pull.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: 'Cannot autofix: this PR is from a forked repository. The autofix workflow can only push to branches within this repository.'
});
core.setFailed('PR is from a fork');
}
core.setOutput('ref', pull.head.ref);
- uses: actions/checkout@v6
if: github.event_name == 'workflow_dispatch' || steps.pr.outcome == 'success'
with:
ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || steps.pr.outputs.ref }}
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
- uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Build
run: pnpm -F svelte build
- name: Run prettier
run: pnpm format
- name: Commit changes
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add -A
git diff --staged --quiet || git commit -m "chore: autofix"
git push origin HEAD

@ -12,26 +12,29 @@ env:
jobs: jobs:
Tests: Tests:
permissions: {}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 15 timeout-minutes: 15
strategy: strategy:
matrix: matrix:
include: include:
- node-version: 18 # Vitest 4 requires Node 20+, so tests run on 20/22/24. The published
# Svelte package still supports Node >=18 (see packages/svelte/package.json).
- node-version: 20
os: windows-latest os: windows-latest
- node-version: 18 - node-version: 20
os: macOS-latest os: macOS-latest
- node-version: 18
os: ubuntu-latest
- node-version: 20 - node-version: 20
os: ubuntu-latest os: ubuntu-latest
- node-version: 22 - node-version: 22
os: ubuntu-latest os: ubuntu-latest
- node-version: 24
os: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v6
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: pnpm cache: pnpm
@ -40,15 +43,50 @@ jobs:
- run: pnpm test - run: pnpm test
env: env:
CI: true CI: true
Lint: TestNoAsync:
permissions: {}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm playwright install chromium
- run: pnpm test runtime-runes
env:
CI: true
SVELTE_NO_ASYNC: true
TSGo:
permissions: {}
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
- name: install
run: pnpm install --frozen-lockfile
- name: install tsgo
run: cd packages/svelte && pnpm i -D @typescript/native-preview
- name: type check
run: cd packages/svelte && pnpm check:tsgo
Lint:
permissions: {}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@v6
with: with:
node-version: 18 node-version: 24
cache: pnpm cache: pnpm
- name: install - name: install
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@ -59,16 +97,19 @@ jobs:
run: pnpm lint run: pnpm lint
- name: build and check generated types - name: build and check generated types
if: (${{ success() }} || ${{ failure() }}) # ensures this step runs even if previous steps fail if: (${{ success() }} || ${{ failure() }}) # ensures this step runs even if previous steps fail
run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally and commit the changes after you have reviewed them"; git diff; exit 1); } run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally with `cd packages/svelte && pnpm generate:types` and commit the changes after you have reviewed them"; git diff; exit 1); }
- name: check browser-support docs page is up to date
run: '{ [ "`git status --porcelain=v1 documentation/docs/07-misc/.generated/`" == "" ] || (echo "The browser-support docs page is out of date — please regenerate it locally with \`cd packages/svelte && pnpm generate:browser-support\` and commit the changes"; git diff documentation/docs/07-misc/.generated/; exit 1); }'
Benchmarks: Benchmarks:
permissions: {}
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v6
with: with:
node-version: 18 node-version: 24
cache: pnpm cache: pnpm
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm bench - run: pnpm bench

@ -1,26 +0,0 @@
# https://github.com/sveltejs/svelte.dev/blob/main/apps/svelte.dev/scripts/sync-docs/README.md
name: Docs preview create request
on:
pull_request_target:
branches:
- main
jobs:
dispatch:
runs-on: ubuntu-latest
steps:
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.SYNC_REQUEST_TOKEN }}
repository: sveltejs/svelte.dev
event-type: docs-preview-create
client-payload: |-
{
"package": "svelte",
"repo": "${{ github.repository }}",
"owner": "${{ github.event.pull_request.head.repo.owner.login }}",
"branch": "${{ github.event.pull_request.head.ref }}",
"pr": ${{ github.event.pull_request.number }}
}

@ -1,27 +0,0 @@
# https://github.com/sveltejs/svelte.dev/blob/main/apps/svelte.dev/scripts/sync-docs/README.md
name: Docs preview delete request
on:
pull_request_target:
branches:
- main
types: [closed]
jobs:
dispatch:
runs-on: ubuntu-latest
steps:
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.SYNC_REQUEST_TOKEN }}
repository: sveltejs/svelte.dev
event-type: docs-preview-delete
client-payload: |-
{
"package": "svelte",
"repo": "${{ github.repository }}",
"owner": "${{ github.event.pull_request.head.repo.owner.login }}",
"branch": "${{ github.event.pull_request.head.ref }}",
"pr": ${{ github.event.pull_request.number }}
}

@ -4,12 +4,21 @@ on:
issue_comment: issue_comment:
types: [created] types: [created]
permissions: {}
jobs: jobs:
trigger: trigger:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run') if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run')
permissions:
issues: write # to add / delete reactions, post comments
pull-requests: write # to read PR data, and to add labels
actions: read # to check workflow status
contents: read # to clone the repo
steps: steps:
- uses: actions/github-script@v6 - name: Check User Permissions
uses: actions/github-script@v8
id: check-permissions
with: with:
script: | script: |
const user = context.payload.sender.login const user = context.payload.sender.login
@ -28,7 +37,7 @@ jobs:
} }
if (hasTriagePermission) { if (hasTriagePermission) {
console.log('Allowed') console.log('User is allowed. Adding +1 reaction.')
await github.rest.reactions.createForIssueComment({ await github.rest.reactions.createForIssueComment({
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
@ -36,16 +45,18 @@ jobs:
content: '+1', content: '+1',
}) })
} else { } else {
console.log('Not allowed') console.log('User is not allowed. Adding -1 reaction.')
await github.rest.reactions.createForIssueComment({ await github.rest.reactions.createForIssueComment({
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
comment_id: context.payload.comment.id, comment_id: context.payload.comment.id,
content: '-1', content: '-1',
}) })
throw new Error('not allowed') throw new Error('User does not have the necessary permissions.')
} }
- uses: actions/github-script@v6
- name: Get PR Data
uses: actions/github-script@v8
id: get-pr-data id: get-pr-data
with: with:
script: | script: |
@ -55,27 +66,65 @@ jobs:
repo: context.repo.repo, repo: context.repo.repo,
pull_number: context.issue.number pull_number: context.issue.number
}) })
const commentCreatedAt = new Date(context.payload.comment.created_at)
const commitPushedAt = new Date(pr.head.repo.pushed_at)
console.log(`Comment created at: ${commentCreatedAt.toISOString()}`)
console.log(`PR last pushed at: ${commitPushedAt.toISOString()}`)
// Check if any commits were pushed after the comment was created
if (commitPushedAt > commentCreatedAt) {
const errorMsg = [
'⚠️ Security warning: PR was updated after the trigger command was posted.',
'',
`Comment posted at: ${commentCreatedAt.toISOString()}`,
`PR last pushed at: ${commitPushedAt.toISOString()}`,
'',
'This could indicate an attempt to inject code after approval.',
'Please review the latest changes and re-run /ecosystem-ci run if they are acceptable.'
].join('\n')
core.setFailed(errorMsg)
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: errorMsg
})
throw new Error('PR was pushed to after comment was created')
}
return { return {
num: context.issue.number, num: context.issue.number,
branchName: pr.head.ref, branchName: pr.head.ref,
commit: pr.head.sha,
repo: pr.head.repo.full_name repo: pr.head.repo.full_name
} }
- id: generate-token
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 #keep pinned for security reasons, currently 1.8.0 - name: Generate Token
id: generate-token
uses: actions/create-github-app-token@v2
with: with:
app_id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }} app-id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }}
private_key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }} private-key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }}
repository: '${{ github.repository_owner }}/svelte-ecosystem-ci' repositories: |
- uses: actions/github-script@v6 svelte
svelte-ecosystem-ci
- name: Trigger Downstream Workflow
uses: actions/github-script@v8
id: trigger id: trigger
env: env:
COMMENT: ${{ github.event.comment.body }} COMMENT: ${{ github.event.comment.body }}
PR_DATA: ${{ steps.get-pr-data.outputs.result }}
with: with:
github-token: ${{ steps.generate-token.outputs.token }} github-token: ${{ steps.generate-token.outputs.token }}
result-encoding: string
script: | script: |
const comment = process.env.COMMENT.trim() const comment = process.env.COMMENT.trim()
const prData = ${{ steps.get-pr-data.outputs.result }} const prData = JSON.parse(process.env.PR_DATA)
const suite = comment.split('\n')[0].replace(/^\/ecosystem-ci run/, '').trim() const suite = comment.split('\n')[0].replace(/^\/ecosystem-ci run/, '').trim()
@ -88,6 +137,7 @@ jobs:
prNumber: '' + prData.num, prNumber: '' + prData.num,
branchName: prData.branchName, branchName: prData.branchName,
repo: prData.repo, repo: prData.repo,
commit: prData.commit,
suite: suite === '' ? '-' : suite suite: suite === '' ? '-' : suite
} }
}) })

@ -1,112 +0,0 @@
name: Update pkg.pr.new comment
on:
workflow_run:
workflows: ['Publish Any Commit']
types:
- completed
jobs:
build:
name: 'Update comment'
runs-on: ubuntu-latest
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: output
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
- run: ls -R .
- name: 'Post or update comment'
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const output = JSON.parse(fs.readFileSync('output.json', 'utf8'));
const bot_comment_identifier = `<!-- pkg.pr.new comment -->`;
const body = (number) => `${bot_comment_identifier}
[Playground](https://svelte.dev/playground?version=pr-${number})
\`\`\`
${output.packages.map((p) => `pnpm add https://pkg.pr.new/${p.name}@${number}`).join('\n')}
\`\`\`
`;
async function find_bot_comment(issue_number) {
if (!issue_number) return null;
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue_number,
});
return comments.data.find((comment) =>
comment.body.includes(bot_comment_identifier)
);
}
async function create_or_update_comment(issue_number) {
if (!issue_number) {
console.log('No issue number provided. Cannot post or update comment.');
return;
}
const existing_comment = await find_bot_comment(issue_number);
if (existing_comment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing_comment.id,
body: body(issue_number),
});
} else {
await github.rest.issues.createComment({
issue_number: issue_number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body(issue_number),
});
}
}
async function log_publish_info() {
const svelte_package = output.packages.find(p => p.name === 'svelte');
const svelte_sha = svelte_package.url.replace(/^.+@([^@]+)$/, '$1');
console.log('\n' + '='.repeat(50));
console.log('Publish Information');
console.log('='.repeat(50));
console.log('\nPublished Packages:');
console.log(output.packages.map((p) => `${p.name} - pnpm add https://pkg.pr.new/${p.name}@${p.url.replace(/^.+@([^@]+)$/, '$1')}`).join('\n'));
if(svelte_sha){
console.log('\nPlayground URL:');
console.log(`\nhttps://svelte.dev/playground?version=commit-${svelte_sha}`)
}
console.log('\n' + '='.repeat(50));
}
if (output.event_name === 'pull_request') {
if (output.number) {
await create_or_update_comment(output.number);
}
} else if (output.event_name === 'push') {
const pull_requests = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${context.repo.owner}:${output.ref.replace('refs/heads/', '')}`,
});
if (pull_requests.data.length > 0) {
await create_or_update_comment(pull_requests.data[0].number);
} else {
console.log(
'No open pull request found for this push. Logging publish information to console:'
);
await log_publish_info();
}
}

@ -1,18 +1,44 @@
name: Publish Any Commit name: pkg.pr.new
on: [push, pull_request] on:
pull_request_target:
types: [opened, synchronize]
push:
branches: [main]
workflow_dispatch:
inputs:
sha:
description: 'Commit SHA to build'
required: true
type: string
pr:
description: 'PR number to comment on'
required: true
type: number
permissions: {}
jobs: jobs:
build: build:
# Skip pull_request_target events from forks — maintainers can use workflow_dispatch instead
if: >
github.event_name != 'pull_request_target' ||
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest runs-on: ubuntu-latest
# No permissions — this job runs user-controlled code
permissions: {}
steps: steps:
- name: Checkout code - uses: actions/checkout@v6
uses: actions/checkout@v4 with:
# For pull_request_target, check out the PR head.
# For workflow_dispatch, check out the manually specified SHA.
# For push, fall back to the push SHA.
ref: ${{ github.event.pull_request.head.sha || inputs.sha || github.sha }}
- run: corepack enable - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v6
with: with:
node-version: 18.x node-version: 22.x
cache: pnpm cache: pnpm
- name: Install dependencies - name: Install dependencies
@ -22,21 +48,182 @@ jobs:
run: pnpm build run: pnpm build
- run: pnpx pkg-pr-new publish --comment=off --json output.json --compact --no-template './packages/svelte' - run: pnpx pkg-pr-new publish --comment=off --json output.json --compact --no-template './packages/svelte'
- name: Add metadata to output
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const output = JSON.parse(fs.readFileSync('output.json', 'utf8'));
output.number = context.issue.number;
output.event_name = context.eventName;
output.ref = context.ref;
fs.writeFileSync('output.json', JSON.stringify(output), 'utf8');
- name: Upload output - name: Upload output
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: output name: output
path: ./output.json path: ./output.json
- run: ls -R . # Sanitizes the untrusted output from the build job before it's consumed by
# jobs with elevated permissions. This ensures that only known package names
# and valid SHA prefixes make it through.
sanitize:
needs: build
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Download artifact
uses: actions/download-artifact@v7
with:
name: output
- name: Sanitize output
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const raw = JSON.parse(fs.readFileSync('output.json', 'utf8'));
const ALLOWED_PACKAGES = new Set(['svelte']);
const SHA_PATTERN = /^[0-9a-f]{7}$/;
const packages = (raw.packages || [])
.filter(p => {
if (!ALLOWED_PACKAGES.has(p.name)) {
console.log(`Skipping unexpected package: ${JSON.stringify(p.name)}`);
return false;
}
const sha = p.url?.replace(/^.+@([^@]+)$/, '$1');
if (!sha || !SHA_PATTERN.test(sha)) {
console.log(`Skipping package with invalid SHA: ${JSON.stringify(p.url)}`);
return false;
}
return true;
})
.map(p => ({
name: p.name,
sha: p.url.replace(/^.+@([^@]+)$/, '$1'),
}));
fs.writeFileSync('sanitized-output.json', JSON.stringify({ packages }), 'utf8');
- name: Upload sanitized output
uses: actions/upload-artifact@v4
with:
name: sanitized-output
path: ./sanitized-output.json
comment:
needs: sanitize
if: github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Download sanitized artifact
uses: actions/download-artifact@v7
with:
name: sanitized-output
- name: Resolve PR number
id: pr
uses: actions/github-script@v8
with:
script: |
if (context.eventName === 'pull_request_target') {
core.setOutput('number', context.issue.number);
return;
}
// For workflow_dispatch, use the explicitly provided PR number.
// We can't use listPullRequestsAssociatedWithCommit because fork
// commits don't exist in the base repo, so the API returns nothing.
const pr = Number('${{ inputs.pr }}');
if (!pr || isNaN(pr)) {
core.setFailed('workflow_dispatch requires a valid pr input');
return;
}
core.setOutput('number', pr);
- name: Post or update comment
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const { packages } = JSON.parse(fs.readFileSync('sanitized-output.json', 'utf8'));
if (packages.length === 0) {
console.log('No valid packages found. Skipping comment.');
return;
}
const issue_number = parseInt('${{ steps.pr.outputs.number }}', 10);
const bot_comment_identifier = `<!-- pkg.pr.new comment -->`;
const body = `${bot_comment_identifier}
[Playground](https://svelte.dev/playground?version=pr-${issue_number})
\`\`\`
${packages.map(p => `pnpm add https://pkg.pr.new/${p.name}@${issue_number}`).join('\n')}
\`\`\`
`;
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number,
});
const existing = comments.data.find(c => c.body.includes(bot_comment_identifier));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number,
body,
});
}
log:
needs: sanitize
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Download sanitized artifact
uses: actions/download-artifact@v7
with:
name: sanitized-output
- name: Log publish info
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const { packages } = JSON.parse(fs.readFileSync('sanitized-output.json', 'utf8'));
if (packages.length === 0) {
console.log('No valid packages found.');
return;
}
console.log('\n' + '='.repeat(50));
console.log('Publish Information');
console.log('='.repeat(50));
for (const p of packages) {
console.log(`${p.name} - pnpm add https://pkg.pr.new/${p.name}@${p.sha}`);
}
const svelte = packages.find(p => p.name === 'svelte');
if (svelte) {
console.log(`\nPlayground: https://svelte.dev/playground?version=commit-${svelte.sha}`);
}
console.log('='.repeat(50));

@ -5,6 +5,11 @@ on:
branches: branches:
- main - main
concurrency:
# prevent two release workflows from running at once
# race conditions here can result in releases failing
group: ${{ github.workflow }}
permissions: {} permissions: {}
jobs: jobs:
release: release:
@ -18,30 +23,29 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Repo - name: Checkout Repo
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0 fetch-depth: 0
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version: 18.x node-version: 24.x
cache: pnpm cache: pnpm
- name: Install - name: Install
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Build - name: Build
run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally and commit the changes after you have reviewed them"; git diff; exit 1); } run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally with `cd packages/svelte && pnpm generate:types` and commit the changes after you have reviewed them"; git diff; exit 1); }
- name: Create Release Pull Request or Publish to npm - name: Create Release Pull Request or Publish to npm
id: changesets id: changesets
uses: changesets/action@v1 uses: changesets/action@6a0a831ff30acef54f2c6aa1cbbc1096b066edaf # v1
with: with:
version: pnpm changeset:version version: pnpm changeset:version
publish: pnpm changeset:publish publish: pnpm changeset:publish
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: true NPM_CONFIG_PROVENANCE: true
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

@ -1,22 +0,0 @@
# https://github.com/sveltejs/svelte.dev/blob/main/apps/svelte.dev/scripts/sync-docs/README.md
name: Sync request
on:
push:
branches:
- main
jobs:
dispatch:
runs-on: ubuntu-latest
steps:
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.SYNC_REQUEST_TOKEN }}
repository: sveltejs/svelte.dev
event-type: sync-request
client-payload: |-
{
"package": "svelte"
}

3
.gitignore vendored

@ -22,5 +22,8 @@ coverage
.DS_Store .DS_Store
tmp tmp
packages/svelte/scripts/_baseline/
benchmarking/.profiles
benchmarking/compare/.results benchmarking/compare/.results
benchmarking/compare/.profiles

@ -7,6 +7,8 @@ packages/**/config/*.js
# packages/svelte # packages/svelte
packages/svelte/messages/**/*.md packages/svelte/messages/**/*.md
packages/svelte/scripts/_bundle.js
packages/svelte/scripts/_baseline/*.ts
packages/svelte/src/compiler/errors.js packages/svelte/src/compiler/errors.js
packages/svelte/src/compiler/warnings.js packages/svelte/src/compiler/warnings.js
packages/svelte/src/internal/client/errors.js packages/svelte/src/internal/client/errors.js
@ -14,6 +16,7 @@ packages/svelte/src/internal/client/warnings.js
packages/svelte/src/internal/shared/errors.js packages/svelte/src/internal/shared/errors.js
packages/svelte/src/internal/shared/warnings.js packages/svelte/src/internal/shared/warnings.js
packages/svelte/src/internal/server/errors.js packages/svelte/src/internal/server/errors.js
packages/svelte/src/internal/server/warnings.js
packages/svelte/tests/migrate/samples/*/output.svelte packages/svelte/tests/migrate/samples/*/output.svelte
packages/svelte/tests/**/*.svelte packages/svelte/tests/**/*.svelte
packages/svelte/tests/**/_expected* packages/svelte/tests/**/_expected*
@ -23,19 +26,15 @@ packages/svelte/tests/**/_output
packages/svelte/tests/**/shards/*.test.js packages/svelte/tests/**/shards/*.test.js
packages/svelte/tests/hydration/samples/*/_expected.html packages/svelte/tests/hydration/samples/*/_expected.html
packages/svelte/tests/hydration/samples/*/_override.html packages/svelte/tests/hydration/samples/*/_override.html
packages/svelte/tests/parser-legacy/samples/*/_actual.json
packages/svelte/tests/parser-legacy/samples/*/output.json
packages/svelte/tests/parser-modern/samples/*/_actual.json
packages/svelte/tests/parser-modern/samples/*/output.json
packages/svelte/types packages/svelte/types
packages/svelte/compiler/index.js packages/svelte/compiler/index.js
playgrounds/sandbox/input/**.svelte playgrounds/sandbox/dist/*
playgrounds/sandbox/output playgrounds/sandbox/output/*
playgrounds/sandbox/src/*
# sites/svelte.dev
sites/svelte.dev/static/svelte-app.json
sites/svelte.dev/scripts/svelte-app/
sites/svelte.dev/src/routes/_components/Supporters/contributors.jpg
sites/svelte.dev/src/routes/_components/Supporters/contributors.js
sites/svelte.dev/src/routes/_components/Supporters/donors.jpg
sites/svelte.dev/src/routes/_components/Supporters/donors.js
sites/svelte.dev/src/lib/generated
**/node_modules **/node_modules
**/.svelte-kit **/.svelte-kit

@ -17,12 +17,6 @@
"useTabs": false, "useTabs": false,
"tabWidth": 2 "tabWidth": 2
} }
},
{
"files": ["sites/svelte-5-preview/src/routes/docs/content/**/*.md"],
"options": {
"printWidth": 60
}
} }
] ]
} }

@ -1,6 +1,3 @@
{ {
"search.exclude": {
"sites/svelte-5-preview/static/*": true
},
"typescript.tsdk": "node_modules/typescript/lib" "typescript.tsdk": "node_modules/typescript/lib"
} }

@ -0,0 +1,2 @@
https://svelte.dev/funding.json

@ -0,0 +1,11 @@
# Svelte Coding Agent Guide
This guide is for AI coding agents working in the Svelte monorepo.
**Important:** Read and follow [`CONTRIBUTING.md`](./CONTRIBUTING.md) as well - it contains essential information about testing, code structure, and contribution guidelines that applies here.
When submitting a PR, you **MUST** read [`PULL_REQUEST_TEMPLATE.md`](./.github/PULL_REQUEST_TEMPLATE.md) and fill it out correctly. **DO NOT** submit a PR without running the full test suite.
## Quick Reference
If asked to do a performance investigation, use the `performance-investigation` skill.

@ -9,7 +9,7 @@ The [Open Source Guides](https://opensource.guide/) website has a collection of
## Get involved ## Get involved
There are many ways to contribute to Svelte, and many of them do not involve writing any code. Here's a few ideas to get started: There are many ways to contribute to Svelte, and many of them do not involve writing any code. Here are a few ideas to get started:
- Simply start using Svelte. Go through the [Getting Started](https://svelte.dev/docs#getting-started) guide. Does everything work as expected? If not, we're always looking for improvements. Let us know by [opening an issue](#reporting-new-issues). - Simply start using Svelte. Go through the [Getting Started](https://svelte.dev/docs#getting-started) guide. Does everything work as expected? If not, we're always looking for improvements. Let us know by [opening an issue](#reporting-new-issues).
- Look through the [open issues](https://github.com/sveltejs/svelte/issues). A good starting point would be issues tagged [good first issue](https://github.com/sveltejs/svelte/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). Provide workarounds, ask for clarification, or suggest labels. Help [triage issues](#triaging-issues-and-pull-requests). - Look through the [open issues](https://github.com/sveltejs/svelte/issues). A good starting point would be issues tagged [good first issue](https://github.com/sveltejs/svelte/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). Provide workarounds, ask for clarification, or suggest labels. Help [triage issues](#triaging-issues-and-pull-requests).
@ -43,7 +43,7 @@ The maintainers meet on the final Saturday of each month. While these meetings a
### Prioritization ### Prioritization
We do our best to review PRs and RFCs as they are sent, but it is difficult to keep up. We welcome help in reviewing PRs, RFCs, and issues. If an item aligns with the current priority on our [roadmap](https://svelte.dev/roadmap), it is more likely to be reviewed quickly. PRs to the most important and active ones repositories get reviewed more quickly while PRs to smaller inactive repos may sit for a bit before we periodically come by and review the pending PRs in a batch. We do our best to review PRs and RFCs as they are sent, but it is difficult to keep up. We welcome help in reviewing PRs, RFCs, and issues. If an item aligns with the current priority on our [roadmap](https://svelte.dev/roadmap), it is more likely to be reviewed quickly. PRs to the most important and active repositories get reviewed more quickly while PRs to smaller inactive repos may sit for a bit before we periodically come by and review the pending PRs in a batch.
## Bugs ## Bugs
@ -51,7 +51,7 @@ We use [GitHub issues](https://github.com/sveltejs/svelte/issues) for our public
If you have questions about using Svelte, contact us on Discord at [svelte.dev/chat](https://svelte.dev/chat), and we will do our best to answer your questions. If you have questions about using Svelte, contact us on Discord at [svelte.dev/chat](https://svelte.dev/chat), and we will do our best to answer your questions.
If you see anything you'd like to be implemented, create a [feature request issue](https://github.com/sveltejs/svelte/issues/new?template=feature_request.yml) If you see anything you'd like to be implemented, create a [feature request issue](https://github.com/sveltejs/svelte/issues/new?template=feature_request.yml).
### Reporting new issues ### Reporting new issues
@ -62,8 +62,6 @@ When [opening a new issue](https://github.com/sveltejs/svelte/issues/new/choose)
## Pull requests ## Pull requests
> HEADS UP: Svelte 5 will likely change a lot on the compiler. For that reason, please don't open PRs that are large in scope, touch more than a couple of files etc. In other words, bug fixes are fine, but big feature PRs will likely not be merged.
### Proposing a change ### Proposing a change
If you would like to request a new feature or enhancement but are not yet thinking about opening a pull request, you can also file an issue with [feature template](https://github.com/sveltejs/svelte/issues/new?template=feature_request.yml). If you would like to request a new feature or enhancement but are not yet thinking about opening a pull request, you can also file an issue with [feature template](https://github.com/sveltejs/svelte/issues/new?template=feature_request.yml).
@ -92,9 +90,9 @@ A good test plan has the exact commands you ran and their output, provides scree
#### Writing tests #### Writing tests
All tests are located in `/test` folder. All tests are located in the `/tests` folder.
Test samples are kept in `/test/xxx/samples` folder. Test samples are kept in `/tests/xxx/samples` folders.
#### Running tests #### Running tests
@ -103,14 +101,14 @@ Test samples are kept in `/test/xxx/samples` folder.
1. To run test, run `pnpm test`. 1. To run test, run `pnpm test`.
1. To run a particular test suite, use `pnpm test <suite-name>`, for example: 1. To run a particular test suite, use `pnpm test <suite-name>`, for example:
```bash ```sh
pnpm test validator pnpm test validator
``` ```
1. To filter tests _within_ a test suite, use `pnpm test <suite-name> -- -t <test-name>`, for example: 1. To filter tests _within_ a test suite, use `pnpm test <suite-name> -t <test-name>`, for example:
```bash ```sh
pnpm test validator -- -t a11y-alt-text pnpm test validator -t a11y-alt-text
``` ```
(You can also do `FILTER=<test-name> pnpm test <suite-name>` which removes other tests rather than simply skipping them — this will result in faster and more compact test results, but it's non-idiomatic. Choose your fighter.) (You can also do `FILTER=<test-name> pnpm test <suite-name>` which removes other tests rather than simply skipping them — this will result in faster and more compact test results, but it's non-idiomatic. Choose your fighter.)

@ -1,4 +1,4 @@
Copyright (c) 2016-24 [these people](https://github.com/sveltejs/svelte/graphs/contributors) Copyright (c) 2016-2025 [Svelte Contributors](https://github.com/sveltejs/svelte/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

@ -1,6 +1,11 @@
[![Cybernetically enhanced web apps: Svelte](https://sveltejs.github.io/assets/banner.png)](https://svelte.dev) <a href="https://svelte.dev">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/banner_dark.png">
<img src="assets/banner.png" alt="Svelte - web development for the rest of us" />
</picture>
</a>
[![license](https://img.shields.io/npm/l/svelte.svg)](LICENSE.md) [![Chat](https://img.shields.io/discord/457912077277855764?label=chat&logo=discord)](https://svelte.dev/chat) [![License](https://img.shields.io/npm/l/svelte.svg)](LICENSE.md) [![Chat](https://img.shields.io/discord/457912077277855764?label=chat&logo=discord)](https://svelte.dev/chat)
## What is Svelte? ## What is Svelte?
@ -24,10 +29,6 @@ You may view [our roadmap](https://svelte.dev/roadmap) if you'd like to see what
Please see the [Contributing Guide](CONTRIBUTING.md) and the [`svelte`](packages/svelte) package for information on contributing to Svelte. Please see the [Contributing Guide](CONTRIBUTING.md) and the [`svelte`](packages/svelte) package for information on contributing to Svelte.
### svelte.dev
The source code for https://svelte.dev lives in the [sites](https://github.com/sveltejs/svelte/tree/master/sites/svelte.dev) folder, with all the documentation right [here](https://github.com/sveltejs/svelte/tree/master/documentation). The site is built with [SvelteKit](https://svelte.dev/docs/kit).
## Is svelte.dev down? ## Is svelte.dev down?
Probably not, but it's possible. If you can't seem to access any `.dev` sites, check out [this SuperUser question and answer](https://superuser.com/q/1413402). Probably not, but it's possible. If you can't seem to access any `.dev` sites, check out [this SuperUser question and answer](https://superuser.com/q/1413402).

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

@ -1,12 +1,6 @@
import { kairo_avoidable_owned, kairo_avoidable_unowned } from './kairo/kairo_avoidable.js'; import fs from 'node:fs';
import { kairo_broad_owned, kairo_broad_unowned } from './kairo/kairo_broad.js'; import path from 'node:path';
import { kairo_deep_owned, kairo_deep_unowned } from './kairo/kairo_deep.js'; import 'svelte/internal/flags/async';
import { kairo_diamond_owned, kairo_diamond_unowned } from './kairo/kairo_diamond.js';
import { kairo_mux_unowned, kairo_mux_owned } from './kairo/kairo_mux.js';
import { kairo_repeated_unowned, kairo_repeated_owned } from './kairo/kairo_repeated.js';
import { kairo_triangle_owned, kairo_triangle_unowned } from './kairo/kairo_triangle.js';
import { kairo_unstable_owned, kairo_unstable_unowned } from './kairo/kairo_unstable.js';
import { mol_bench_owned, mol_bench_unowned } from './mol_bench.js';
import { import {
sbench_create_0to1, sbench_create_0to1,
sbench_create_1000to1, sbench_create_1000to1,
@ -19,10 +13,14 @@ import {
sbench_create_4to1, sbench_create_4to1,
sbench_create_signals sbench_create_signals
} from './sbench.js'; } from './sbench.js';
import { fileURLToPath } from 'node:url';
import { create_test } from './util.js';
// This benchmark has been adapted from the js-reactivity-benchmark (https://github.com/milomg/js-reactivity-benchmark) // This benchmark has been adapted from the js-reactivity-benchmark (https://github.com/milomg/js-reactivity-benchmark)
// Not all tests are the same, and many parts have been tweaked to capture different data. // Not all tests are the same, and many parts have been tweaked to capture different data.
const dirname = path.dirname(fileURLToPath(import.meta.url));
export const reactivity_benchmarks = [ export const reactivity_benchmarks = [
sbench_create_signals, sbench_create_signals,
sbench_create_0to1, sbench_create_0to1,
@ -33,23 +31,16 @@ export const reactivity_benchmarks = [
sbench_create_1to2, sbench_create_1to2,
sbench_create_1to4, sbench_create_1to4,
sbench_create_1to8, sbench_create_1to8,
sbench_create_1to1000, sbench_create_1to1000
kairo_avoidable_owned,
kairo_avoidable_unowned,
kairo_broad_owned,
kairo_broad_unowned,
kairo_deep_owned,
kairo_deep_unowned,
kairo_diamond_owned,
kairo_diamond_unowned,
kairo_triangle_owned,
kairo_triangle_unowned,
kairo_mux_owned,
kairo_mux_unowned,
kairo_repeated_owned,
kairo_repeated_unowned,
kairo_unstable_owned,
kairo_unstable_unowned,
mol_bench_owned,
mol_bench_unowned
]; ];
for (const file of fs.readdirSync(`${dirname}/tests`)) {
if (!file.includes('.bench.')) continue;
const name = file.replace('.bench.js', '');
const module = await import(`${dirname}/tests/${file}`);
const { owned, unowned } = create_test(name, module.default);
reactivity_benchmarks.push(owned, unowned);
}

@ -1,91 +0,0 @@
import { assert, fastest_test } from '../../../utils.js';
import * as $ from 'svelte/internal/client';
import { busy } from './util.js';
function setup() {
let head = $.state(0);
let computed1 = $.derived(() => $.get(head));
let computed2 = $.derived(() => ($.get(computed1), 0));
let computed3 = $.derived(() => (busy(), $.get(computed2) + 1)); // heavy computation
let computed4 = $.derived(() => $.get(computed3) + 2);
let computed5 = $.derived(() => $.get(computed4) + 3);
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(computed5);
busy(); // heavy side effect
});
});
return {
destroy,
run() {
$.flush_sync(() => {
$.set(head, 1);
});
assert($.get(computed5) === 6);
for (let i = 0; i < 1000; i++) {
$.flush_sync(() => {
$.set(head, i);
});
assert($.get(computed5) === 6);
}
}
};
}
export async function kairo_avoidable_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_avoidable_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_avoidable_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_avoidable_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -1,97 +0,0 @@
import { assert, fastest_test } from '../../../utils.js';
import * as $ from 'svelte/internal/client';
function setup() {
let head = $.state(0);
let last = head;
let counter = 0;
const destroy = $.effect_root(() => {
for (let i = 0; i < 50; i++) {
let current = $.derived(() => {
return $.get(head) + i;
});
let current2 = $.derived(() => {
return $.get(current) + 1;
});
$.render_effect(() => {
$.get(current2);
counter++;
});
last = current2;
}
});
return {
destroy,
run() {
$.flush_sync(() => {
$.set(head, 1);
});
counter = 0;
for (let i = 0; i < 50; i++) {
$.flush_sync(() => {
$.set(head, i);
});
assert($.get(last) === i + 50);
}
assert(counter === 50 * 50);
}
};
}
export async function kairo_broad_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_broad_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_broad_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_broad_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -1,97 +0,0 @@
import { assert, fastest_test } from '../../../utils.js';
import * as $ from 'svelte/internal/client';
let len = 50;
const iter = 50;
function setup() {
let head = $.state(0);
let current = head;
for (let i = 0; i < len; i++) {
let c = current;
current = $.derived(() => {
return $.get(c) + 1;
});
}
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(current);
counter++;
});
});
return {
destroy,
run() {
$.flush_sync(() => {
$.set(head, 1);
});
counter = 0;
for (let i = 0; i < iter; i++) {
$.flush_sync(() => {
$.set(head, i);
});
assert($.get(current) === len + i);
}
assert(counter === iter);
}
};
}
export async function kairo_deep_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_deep_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_deep_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_deep_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -1,101 +0,0 @@
import { assert, fastest_test } from '../../../utils.js';
import * as $ from 'svelte/internal/client';
let width = 5;
function setup() {
let head = $.state(0);
let current = [];
for (let i = 0; i < width; i++) {
current.push(
$.derived(() => {
return $.get(head) + 1;
})
);
}
let sum = $.derived(() => {
return current.map((x) => $.get(x)).reduce((a, b) => a + b, 0);
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(sum);
counter++;
});
});
return {
destroy,
run() {
$.flush_sync(() => {
$.set(head, 1);
});
assert($.get(sum) === 2 * width);
counter = 0;
for (let i = 0; i < 500; i++) {
$.flush_sync(() => {
$.set(head, i);
});
assert($.get(sum) === (i + 1) * width);
}
assert(counter === 500);
}
};
}
export async function kairo_diamond_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_diamond_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_diamond_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_diamond_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -1,94 +0,0 @@
import { assert, fastest_test } from '../../../utils.js';
import * as $ from 'svelte/internal/client';
function setup() {
let heads = new Array(100).fill(null).map((_) => $.state(0));
const mux = $.derived(() => {
return Object.fromEntries(heads.map((h) => $.get(h)).entries());
});
const splited = heads
.map((_, index) => $.derived(() => $.get(mux)[index]))
.map((x) => $.derived(() => $.get(x) + 1));
const destroy = $.effect_root(() => {
splited.forEach((x) => {
$.render_effect(() => {
$.get(x);
});
});
});
return {
destroy,
run() {
for (let i = 0; i < 10; i++) {
$.flush_sync(() => {
$.set(heads[i], i);
});
assert($.get(splited[i]) === i + 1);
}
for (let i = 0; i < 10; i++) {
$.flush_sync(() => {
$.set(heads[i], i * 2);
});
assert($.get(splited[i]) === i * 2 + 1);
}
}
};
}
export async function kairo_mux_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_mux_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_mux_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_mux_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -1,98 +0,0 @@
import { assert, fastest_test } from '../../../utils.js';
import * as $ from 'svelte/internal/client';
let size = 30;
function setup() {
let head = $.state(0);
let current = $.derived(() => {
let result = 0;
for (let i = 0; i < size; i++) {
result += $.get(head);
}
return result;
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(current);
counter++;
});
});
return {
destroy,
run() {
$.flush_sync(() => {
$.set(head, 1);
});
assert($.get(current) === size);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush_sync(() => {
$.set(head, i);
});
assert($.get(current) === i * size);
}
assert(counter === 100);
}
};
}
export async function kairo_repeated_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_repeated_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_repeated_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_repeated_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -1,111 +0,0 @@
import { assert, fastest_test } from '../../../utils.js';
import * as $ from 'svelte/internal/client';
let width = 10;
function count(number) {
return new Array(number)
.fill(0)
.map((_, i) => i + 1)
.reduce((x, y) => x + y, 0);
}
function setup() {
let head = $.state(0);
let current = head;
let list = [];
for (let i = 0; i < width; i++) {
let c = current;
list.push(current);
current = $.derived(() => {
return $.get(c) + 1;
});
}
let sum = $.derived(() => {
return list.map((x) => $.get(x)).reduce((a, b) => a + b, 0);
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(sum);
counter++;
});
});
return {
destroy,
run() {
const constant = count(width);
$.flush_sync(() => {
$.set(head, 1);
});
assert($.get(sum) === constant);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush_sync(() => {
$.set(head, i);
});
assert($.get(sum) === constant - width + i * width);
}
assert(counter === 100);
}
};
}
export async function kairo_triangle_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_triangle_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_triangle_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_triangle_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -1,97 +0,0 @@
import { assert, fastest_test } from '../../../utils.js';
import * as $ from 'svelte/internal/client';
function setup() {
let head = $.state(0);
const double = $.derived(() => $.get(head) * 2);
const inverse = $.derived(() => -$.get(head));
let current = $.derived(() => {
let result = 0;
for (let i = 0; i < 20; i++) {
result += $.get(head) % 2 ? $.get(double) : $.get(inverse);
}
return result;
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(current);
counter++;
});
});
return {
destroy,
run() {
$.flush_sync(() => {
$.set(head, 1);
});
assert($.get(current) === 40);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush_sync(() => {
$.set(head, i);
});
}
assert(counter === 100);
}
};
}
export async function kairo_unstable_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
destroy();
return {
benchmark: 'kairo_unstable_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function kairo_unstable_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run();
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
run();
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'kairo_unstable_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -1,6 +0,0 @@
export function busy() {
let a = 0;
for (let i = 0; i < 1_00; i++) {
a++;
}
}

@ -1,3 +1,4 @@
/** @import { Source } from '../../../packages/svelte/src/internal/client/types.js' */
import { fastest_test } from '../../utils.js'; import { fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js'; import * as $ from '../../../packages/svelte/src/internal/client/index.js';
@ -7,360 +8,177 @@ const COUNT = 1e5;
* @param {number} n * @param {number} n
* @param {any[]} sources * @param {any[]} sources
*/ */
function create_data_signals(n, sources) { function create_sources(n, sources) {
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
sources[i] = $.state(i); sources[i] = $.state(i);
} }
return sources;
}
/**
* @param {number} i
*/
function create_computation_0(i) {
$.derived(() => i);
}
/**
* @param {any} s1
*/
function create_computation_1(s1) {
$.derived(() => $.get(s1));
}
/**
* @param {any} s1
* @param {any} s2
*/
function create_computation_2(s1, s2) {
$.derived(() => $.get(s1) + $.get(s2));
}
function create_computation_1000(ss, offset) {
$.derived(() => {
let sum = 0;
for (let i = 0; i < 1000; i++) {
sum += $.get(ss[offset + i]);
}
return sum;
});
}
/**
* @param {number} n
*/
function create_computations_0to1(n) {
for (let i = 0; i < n; i++) {
create_computation_0(i);
}
}
/** return sources;
* @param {number} n
* @param {any[]} sources
*/
function create_computations_1to1(n, sources) {
for (let i = 0; i < n; i++) {
const source = sources[i];
create_computation_1(source);
}
} }
/** /**
* @param {number} n * @param {Source<number>} source
* @param {any[]} sources
*/ */
function create_computations_2to1(n, sources) { function create_derived(source) {
for (let i = 0; i < n; i++) { $.derived(() => $.get(source));
create_computation_2(sources[i * 2], sources[i * 2 + 1]);
}
}
function create_computation_4(s1, s2, s3, s4) {
$.derived(() => $.get(s1) + $.get(s2) + $.get(s3) + $.get(s4));
}
function create_computations_1000to1(n, sources) {
for (let i = 0; i < n; i++) {
create_computation_1000(sources, i * 1000);
}
}
function create_computations_1to2(n, sources) {
for (let i = 0; i < n / 2; i++) {
const source = sources[i];
create_computation_1(source);
create_computation_1(source);
}
}
function create_computations_1to4(n, sources) {
for (let i = 0; i < n / 4; i++) {
const source = sources[i];
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
}
}
function create_computations_1to8(n, sources) {
for (let i = 0; i < n / 8; i++) {
const source = sources[i];
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
create_computation_1(source);
}
}
function create_computations_1to1000(n, sources) {
for (let i = 0; i < n / 1000; i++) {
const source = sources[i];
for (let j = 0; j < 1000; j++) {
create_computation_1(source);
}
}
}
function create_computations_4to1(n, sources) {
for (let i = 0; i < n; i++) {
create_computation_4(
sources[i * 4],
sources[i * 4 + 1],
sources[i * 4 + 2],
sources[i * 4 + 3]
);
}
} }
/** /**
* @param {any} fn *
* @param {string} label
* @param {(n: number, sources: Array<Source<number>>) => void} fn
* @param {number} count * @param {number} count
* @param {number} scount * @param {number} num_sources
*/ */
function bench(fn, count, scount) { function create_sbench_test(label, count, num_sources, fn) {
let sources = create_data_signals(scount, []);
fn(count, sources);
}
export async function sbench_create_signals() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_data_signals, COUNT, COUNT);
}
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
bench(create_data_signals, COUNT, COUNT);
}
});
return { return {
benchmark: 'sbench_create_signals', label,
time: timing.time.toFixed(2), fn: async () => {
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_0to1() {
// Do 3 loops to warm up JIT // Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
bench(create_computations_0to1, COUNT, 0); fn(count, create_sources(num_sources, []));
} }
const { timing } = await fastest_test(10, () => { return await fastest_test(10, () => {
const destroy = $.effect_root(() => { const destroy = $.effect_root(() => {
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
bench(create_computations_0to1, COUNT, 0); fn(count, create_sources(num_sources, []));
} }
}); });
destroy(); destroy();
}); });
return {
benchmark: 'sbench_create_0to1',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_1to1() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_1to1, COUNT, COUNT);
}
const { timing } = await fastest_test(10, () => {
const destroy = $.effect_root(() => {
for (let i = 0; i < 10; i++) {
bench(create_computations_1to1, COUNT, COUNT);
} }
});
destroy();
});
return {
benchmark: 'sbench_create_1to1',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
}; };
} }
export async function sbench_create_2to1() { export const sbench_create_signals = create_sbench_test(
// Do 3 loops to warm up JIT 'sbench_create_signals',
for (let i = 0; i < 3; i++) { COUNT,
bench(create_computations_2to1, COUNT / 2, COUNT); COUNT,
} create_sources
);
const { timing } = await fastest_test(10, () => { export const sbench_create_0to1 = create_sbench_test('sbench_create_0to1', COUNT, 0, (n) => {
const destroy = $.effect_root(() => { for (let i = 0; i < n; i++) {
for (let i = 0; i < 10; i++) { $.derived(() => i);
bench(create_computations_2to1, COUNT / 2, COUNT);
} }
}); });
destroy();
});
return {
benchmark: 'sbench_create_2to1',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}
export async function sbench_create_4to1() { export const sbench_create_1to1 = create_sbench_test(
// Do 3 loops to warm up JIT 'sbench_create_1to1',
for (let i = 0; i < 3; i++) { COUNT,
bench(create_computations_4to1, COUNT / 4, COUNT); COUNT,
(n, sources) => {
for (let i = 0; i < n; i++) {
create_derived(sources[i]);
} }
const { timing } = await fastest_test(10, () => {
const destroy = $.effect_root(() => {
for (let i = 0; i < 10; i++) {
bench(create_computations_4to1, COUNT / 4, COUNT);
} }
}); );
destroy();
});
return { export const sbench_create_2to1 = create_sbench_test(
benchmark: 'sbench_create_4to1', 'sbench_create_2to1',
time: timing.time.toFixed(2), COUNT / 2,
gc_time: timing.gc_time.toFixed(2) COUNT,
}; (n, sources) => {
for (let i = 0; i < n; i++) {
$.derived(() => $.get(sources[i * 2]) + $.get(sources[i * 2 + 1]));
} }
export async function sbench_create_1000to1() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_1000to1, COUNT / 1000, COUNT);
} }
);
const { timing } = await fastest_test(10, () => { export const sbench_create_4to1 = create_sbench_test(
const destroy = $.effect_root(() => { 'sbench_create_4to1',
for (let i = 0; i < 10; i++) { COUNT / 4,
bench(create_computations_1000to1, COUNT / 1000, COUNT); COUNT,
(n, sources) => {
for (let i = 0; i < n; i++) {
$.derived(
() =>
$.get(sources[i * 4]) +
$.get(sources[i * 4 + 1]) +
$.get(sources[i * 4 + 2]) +
$.get(sources[i * 4 + 3])
);
} }
});
destroy();
});
return {
benchmark: 'sbench_create_1000to1',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
} }
);
export async function sbench_create_1to2() { export const sbench_create_1000to1 = create_sbench_test(
// Do 3 loops to warm up JIT 'sbench_create_1000to1',
for (let i = 0; i < 3; i++) { COUNT / 1000,
bench(create_computations_1to2, COUNT, COUNT / 2); COUNT,
} (n, sources) => {
for (let i = 0; i < n; i++) {
const offset = i * 1000;
const { timing } = await fastest_test(10, () => { $.derived(() => {
const destroy = $.effect_root(() => { let sum = 0;
for (let i = 0; i < 10; i++) { for (let i = 0; i < 1000; i++) {
bench(create_computations_1to2, COUNT, COUNT / 2); sum += $.get(sources[offset + i]);
} }
return sum;
}); });
destroy();
});
return {
benchmark: 'sbench_create_1to2',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
} }
export async function sbench_create_1to4() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_1to4, COUNT, COUNT / 4);
} }
);
const { timing } = await fastest_test(10, () => { export const sbench_create_1to2 = create_sbench_test(
const destroy = $.effect_root(() => { 'sbench_create_1to2',
for (let i = 0; i < 10; i++) { COUNT,
bench(create_computations_1to4, COUNT, COUNT / 4); COUNT / 2,
(n, sources) => {
for (let i = 0; i < n / 2; i++) {
const source = sources[i];
create_derived(source);
create_derived(source);
} }
});
destroy();
});
return {
benchmark: 'sbench_create_1to4',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
} }
);
export async function sbench_create_1to8() { export const sbench_create_1to4 = create_sbench_test(
// Do 3 loops to warm up JIT 'sbench_create_1to4',
for (let i = 0; i < 3; i++) { COUNT,
bench(create_computations_1to8, COUNT, COUNT / 8); COUNT / 4,
(n, sources) => {
for (let i = 0; i < n / 4; i++) {
const source = sources[i];
create_derived(source);
create_derived(source);
create_derived(source);
create_derived(source);
} }
const { timing } = await fastest_test(10, () => {
const destroy = $.effect_root(() => {
for (let i = 0; i < 10; i++) {
bench(create_computations_1to8, COUNT, COUNT / 8);
} }
}); );
destroy();
});
return { export const sbench_create_1to8 = create_sbench_test(
benchmark: 'sbench_create_1to8', 'sbench_create_1to8',
time: timing.time.toFixed(2), COUNT,
gc_time: timing.gc_time.toFixed(2) COUNT / 8,
}; (n, sources) => {
for (let i = 0; i < n / 8; i++) {
const source = sources[i];
create_derived(source);
create_derived(source);
create_derived(source);
create_derived(source);
create_derived(source);
create_derived(source);
create_derived(source);
create_derived(source);
} }
export async function sbench_create_1to1000() {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
bench(create_computations_1to1000, COUNT, COUNT / 1000);
} }
);
const { timing } = await fastest_test(10, () => { export const sbench_create_1to1000 = create_sbench_test(
const destroy = $.effect_root(() => { 'sbench_create_1to1000',
for (let i = 0; i < 10; i++) { COUNT,
bench(create_computations_1to1000, COUNT, COUNT / 1000); COUNT / 1000,
(n, sources) => {
for (let i = 0; i < n / 1000; i++) {
const source = sources[i];
for (let j = 0; j < 1000; j++) {
create_derived(source);
}
} }
});
destroy();
});
return {
benchmark: 'sbench_create_1to1000',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
} }
);

@ -0,0 +1,32 @@
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
export default () => {
const a = $.state(1);
const b = $.state(2);
let total = 0;
const destroy = $.effect_root(() => {
for (let i = 0; i < 1000; i += 1) {
$.render_effect(() => {
total += $.get(a);
});
}
$.render_effect(() => {
total += $.get(b);
});
});
return {
destroy,
run() {
for (let i = 0; i < 5; i++) {
total = 0;
$.flush(() => $.set(b, i));
assert.equal(total, i);
}
}
};
};

@ -0,0 +1,35 @@
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
import { busy } from '../util.js';
export default () => {
let head = $.state(0);
let computed1 = $.derived(() => $.get(head));
let computed2 = $.derived(() => ($.get(computed1), 0));
let computed3 = $.derived(() => (busy(), $.get(computed2) + 1)); // heavy computation
let computed4 = $.derived(() => $.get(computed3) + 2);
let computed5 = $.derived(() => $.get(computed4) + 3);
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(computed5);
busy(); // heavy side effect
});
});
return {
destroy,
run() {
$.flush(() => {
$.set(head, 1);
});
assert.equal($.get(computed5), 6);
for (let i = 0; i < 1000; i++) {
$.flush(() => {
$.set(head, i);
});
assert.equal($.get(computed5), 6);
}
}
};
};

@ -0,0 +1,41 @@
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
export default () => {
let head = $.state(0);
let last = head;
let counter = 0;
const destroy = $.effect_root(() => {
for (let i = 0; i < 50; i++) {
let current = $.derived(() => {
return $.get(head) + i;
});
let current2 = $.derived(() => {
return $.get(current) + 1;
});
$.render_effect(() => {
$.get(current2);
counter++;
});
last = current2;
}
});
return {
destroy,
run() {
$.flush(() => {
$.set(head, 1);
});
counter = 0;
for (let i = 0; i < 50; i++) {
$.flush(() => {
$.set(head, i);
});
assert.equal($.get(last), i + 50);
}
assert.equal(counter, 50 * 50);
}
};
};

@ -0,0 +1,41 @@
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
let len = 50;
const iter = 50;
export default () => {
let head = $.state(0);
let current = head;
for (let i = 0; i < len; i++) {
let c = current;
current = $.derived(() => {
return $.get(c) + 1;
});
}
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(current);
counter++;
});
});
return {
destroy,
run() {
$.flush(() => {
$.set(head, 1);
});
counter = 0;
for (let i = 0; i < iter; i++) {
$.flush(() => {
$.set(head, i);
});
assert.equal($.get(current), len + i);
}
assert.equal(counter, iter);
}
};
};

@ -0,0 +1,45 @@
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
let width = 5;
export default () => {
let head = $.state(0);
let current = [];
for (let i = 0; i < width; i++) {
current.push(
$.derived(() => {
return $.get(head) + 1;
})
);
}
let sum = $.derived(() => {
return current.map((x) => $.get(x)).reduce((a, b) => a + b, 0);
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(sum);
counter++;
});
});
return {
destroy,
run() {
$.flush(() => {
$.set(head, 1);
});
assert.equal($.get(sum), 2 * width);
counter = 0;
for (let i = 0; i < 500; i++) {
$.flush(() => {
$.set(head, i);
});
assert.equal($.get(sum), (i + 1) * width);
}
assert.equal(counter, 500);
}
};
};

@ -0,0 +1,38 @@
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
export default () => {
let heads = new Array(100).fill(null).map((_) => $.state(0));
const mux = $.derived(() => {
return Object.fromEntries(heads.map((h) => $.get(h)).entries());
});
const splited = heads
.map((_, index) => $.derived(() => $.get(mux)[index]))
.map((x) => $.derived(() => $.get(x) + 1));
const destroy = $.effect_root(() => {
splited.forEach((x) => {
$.render_effect(() => {
$.get(x);
});
});
});
return {
destroy,
run() {
for (let i = 0; i < 10; i++) {
$.flush(() => {
$.set(heads[i], i);
});
assert.equal($.get(splited[i]), i + 1);
}
for (let i = 0; i < 10; i++) {
$.flush(() => {
$.set(heads[i], i * 2);
});
assert.equal($.get(splited[i]), i * 2 + 1);
}
}
};
};

@ -0,0 +1,42 @@
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
let size = 30;
export default () => {
let head = $.state(0);
let current = $.derived(() => {
let result = 0;
for (let i = 0; i < size; i++) {
result += $.get(head);
}
return result;
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(current);
counter++;
});
});
return {
destroy,
run() {
$.flush(() => {
$.set(head, 1);
});
assert.equal($.get(current), size);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush(() => {
$.set(head, i);
});
assert.equal($.get(current), i * size);
}
assert.equal(counter, 100);
}
};
};

@ -0,0 +1,55 @@
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
let width = 10;
function count(number) {
return new Array(number)
.fill(0)
.map((_, i) => i + 1)
.reduce((x, y) => x + y, 0);
}
export default () => {
let head = $.state(0);
let current = head;
let list = [];
for (let i = 0; i < width; i++) {
let c = current;
list.push(current);
current = $.derived(() => {
return $.get(c) + 1;
});
}
let sum = $.derived(() => {
return list.map((x) => $.get(x)).reduce((a, b) => a + b, 0);
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(sum);
counter++;
});
});
return {
destroy,
run() {
const constant = count(width);
$.flush(() => {
$.set(head, 1);
});
assert.equal($.get(sum), constant);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush(() => {
$.set(head, i);
});
assert.equal($.get(sum), constant - width + i * width);
}
assert.equal(counter, 100);
}
};
};

@ -0,0 +1,41 @@
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
export default () => {
let head = $.state(0);
const double = $.derived(() => $.get(head) * 2);
const inverse = $.derived(() => -$.get(head));
let current = $.derived(() => {
let result = 0;
for (let i = 0; i < 20; i++) {
result += $.get(head) % 2 ? $.get(double) : $.get(inverse);
}
return result;
});
let counter = 0;
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(current);
counter++;
});
});
return {
destroy,
run() {
$.flush(() => {
$.set(head, 1);
});
assert.equal($.get(current), 40);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush(() => {
$.set(head, i);
});
}
assert.equal(counter, 100);
}
};
};

@ -1,4 +1,4 @@
import { assert, fastest_test } from '../../utils.js'; import assert from 'node:assert';
import * as $ from 'svelte/internal/client'; import * as $ from 'svelte/internal/client';
/** /**
@ -18,7 +18,7 @@ function hard(n) {
const numbers = Array.from({ length: 5 }, (_, i) => i); const numbers = Array.from({ length: 5 }, (_, i) => i);
function setup() { export default () => {
let res = []; let res = [];
const A = $.state(0); const A = $.state(0);
const B = $.state(0); const B = $.state(0);
@ -51,71 +51,18 @@ function setup() {
*/ */
run(i) { run(i) {
res.length = 0; res.length = 0;
$.flush_sync(() => { $.flush(() => {
$.set(B, 1); $.set(B, 1);
$.set(A, 1 + i * 2); $.set(A, 1 + i * 2);
}); });
$.flush_sync(() => { $.flush(() => {
$.set(A, 2 + i * 2); $.set(A, 2 + i * 2);
$.set(B, 2); $.set(B, 2);
}); });
assert(res[0] === 3198 && res[1] === 1601 && res[2] === 3195 && res[3] === 1598); assert.equal(res[0], 3198);
assert.equal(res[1], 1601);
assert.equal(res[2], 3195);
assert.equal(res[3], 1598);
} }
}; };
}
export async function mol_bench_owned() {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run(0);
destroy();
}
({ run, destroy } = setup());
});
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1e4; i++) {
run(i);
}
});
// @ts-ignore
destroy();
destroy_owned();
return {
benchmark: 'mol_bench_owned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
}; };
}
export async function mol_bench_unowned() {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run(0);
destroy();
}
const { run, destroy } = setup();
const { timing } = await fastest_test(10, () => {
for (let i = 0; i < 1e4; i++) {
run(i);
}
});
destroy();
return {
benchmark: 'mol_bench_unowned',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
};
}

@ -0,0 +1,35 @@
import assert from 'node:assert';
import * as $ from 'svelte/internal/client';
const ARRAY_SIZE = 1000;
export default () => {
const signals = Array.from({ length: ARRAY_SIZE }, (_, i) => $.state(i));
const order = $.state(0);
// break skipped_deps fast path by changing order of reads
const total = $.derived(() => {
const ord = $.get(order);
let sum = 0;
for (let i = 0; i < ARRAY_SIZE; i++) {
sum += /** @type {number} */ ($.get(signals[(i + ord) % ARRAY_SIZE]));
}
return sum;
});
const destroy = $.effect_root(() => {
$.render_effect(() => {
$.get(total);
});
});
return {
destroy,
run() {
for (let i = 0; i < 5; i++) {
$.flush(() => $.set(order, i));
assert.equal($.get(total), (ARRAY_SIZE * (ARRAY_SIZE - 1)) / 2); // sum of 0..999
}
}
};
};

@ -0,0 +1,71 @@
import * as $ from 'svelte/internal/client';
import { fastest_test } from '../../utils.js';
export function busy() {
let a = 0;
for (let i = 0; i < 1_00; i++) {
a++;
}
}
/**
*
* @param {string} label
* @param {() => { run: (i?: number) => void, destroy: () => void }} setup
*/
export function create_test(label, setup) {
return {
unowned: {
label: `${label}_unowned`,
fn: async () => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run(0);
destroy();
}
const { run, destroy } = setup();
const result = await fastest_test(10, () => {
for (let i = 0; i < 1000; i++) {
run(i);
}
});
destroy();
return result;
}
},
owned: {
label: `${label}_owned`,
fn: async () => {
let run, destroy;
const destroy_owned = $.effect_root(() => {
// Do 10 loops to warm up JIT
for (let i = 0; i < 10; i++) {
const { run, destroy } = setup();
run(0);
destroy();
}
({ run, destroy } = setup());
});
const result = await fastest_test(10, () => {
for (let i = 0; i < 1000; i++) {
run(i);
}
});
// @ts-ignore
destroy();
destroy_owned();
return result;
}
}
};
}

@ -1,13 +1,16 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { render } from 'svelte/server'; import { render } from 'svelte/server';
import { fastest_test, read_file, write } from '../../../utils.js'; import { fastest_test } from '../../../utils.js';
import { compile } from 'svelte/compiler'; import { compile } from 'svelte/compiler';
const dir = `${process.cwd()}/benchmarking/benchmarks/ssr/wrapper`; const dir = `${process.cwd()}/benchmarking/benchmarks/ssr/wrapper`;
async function compile_svelte() { async function compile_svelte() {
const output = compile(read_file(`${dir}/App.svelte`), { const output = compile(read(`${dir}/App.svelte`), {
generate: 'server' generate: 'server'
}); });
write(`${dir}/output/App.js`, output.js.code); write(`${dir}/output/App.js`, output.js.code);
const module = await import(`${dir}/output/App.js`); const module = await import(`${dir}/output/App.js`);
@ -15,22 +18,39 @@ async function compile_svelte() {
return module.default; return module.default;
} }
export async function wrapper_bench() { export const wrapper_bench = {
label: 'wrapper_bench',
fn: async () => {
const App = await compile_svelte(); const App = await compile_svelte();
// Do 3 loops to warm up JIT // Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
render(App); render(App);
} }
const { timing } = await fastest_test(10, () => { return await fastest_test(10, () => {
for (let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {
render(App); render(App);
} }
}); });
}
return {
benchmark: 'wrapper_bench',
time: timing.time.toFixed(2),
gc_time: timing.gc_time.toFixed(2)
}; };
/**
* @param {string} file
*/
function read(file) {
return fs.readFileSync(file, 'utf-8').replace(/\r\n/g, '\n');
}
/**
* @param {string} file
* @param {string} contents
*/
function write(file, contents) {
try {
fs.mkdirSync(path.dirname(file), { recursive: true });
} catch {}
fs.writeFileSync(file, contents);
} }

@ -0,0 +1,81 @@
import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
export function generate_report(outdir) {
const result_files = fs
.readdirSync(outdir)
.filter((file) => file.endsWith('.json'))
.sort((a, b) => a.localeCompare(b));
const branches = result_files.map((file) => file.slice(0, -5));
const results = result_files.map((file) =>
JSON.parse(fs.readFileSync(`${outdir}/${file}`, 'utf-8'))
);
if (results.length === 0) {
console.error(`No result files found in ${outdir}`);
process.exit(1);
}
const report_file = path.join(outdir, 'report.txt');
fs.writeFileSync(report_file, '');
const write = (str) => {
fs.appendFileSync(report_file, str + '\n');
console.log(str);
};
for (let i = 0; i < branches.length; i += 1) {
write(`${char(i)}: ${branches[i]}`);
}
write('');
for (let i = 0; i < results[0].length; i += 1) {
write(`${results[0][i].benchmark}`);
for (const metric of ['time', 'gc_time']) {
const times = results.map((result) => +result[i][metric]);
let min = Infinity;
let max = -Infinity;
let min_index = -1;
for (let b = 0; b < times.length; b += 1) {
const time = times[b];
if (time < min) {
min = time;
min_index = b;
}
if (time > max) {
max = time;
}
}
if (min !== 0) {
write(` ${metric}: fastest is ${char(min_index)} (${branches[min_index]})`);
times.forEach((time, b) => {
const SIZE = 20;
const n = Math.round(SIZE * (time / max));
write(` ${char(b)}: ${'◼'.repeat(n)}${' '.repeat(SIZE - n)} ${time.toFixed(2)}ms`);
});
}
}
write('');
}
}
function char(i) {
return String.fromCharCode(97 + i);
}
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
const outdir = path.resolve(process.argv[1], '../.results');
generate_report(outdir);
}

@ -2,7 +2,8 @@ import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { execSync, fork } from 'node:child_process'; import { execSync, fork } from 'node:child_process';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { benchmarks } from '../benchmarks.js'; import { safe } from '../utils.js';
import { generate_report } from './generate-report.js';
// if (execSync('git status --porcelain').toString().trim()) { // if (execSync('git status --porcelain').toString().trim()) {
// console.error('Working directory is not clean'); // console.error('Working directory is not clean');
@ -13,39 +14,61 @@ const filename = fileURLToPath(import.meta.url);
const runner = path.resolve(filename, '../runner.js'); const runner = path.resolve(filename, '../runner.js');
const outdir = path.resolve(filename, '../.results'); const outdir = path.resolve(filename, '../.results');
if (fs.existsSync(outdir)) fs.rmSync(outdir, { recursive: true }); fs.mkdirSync(outdir, { recursive: true });
fs.mkdirSync(outdir);
const branches = []; const requested_branches = [];
let PROFILE_DIR = path.resolve(filename, '../.profiles');
fs.mkdirSync(PROFILE_DIR, { recursive: true });
for (const arg of process.argv.slice(2)) { for (const arg of process.argv.slice(2)) {
if (arg.startsWith('--')) continue; if (arg.startsWith('--')) continue;
if (arg === filename) continue; if (arg === filename) continue;
branches.push(arg); requested_branches.push(arg);
} }
if (branches.length === 0) { if (requested_branches.length === 0) {
branches.push( requested_branches.push(
execSync('git symbolic-ref --short -q HEAD || git rev-parse --short HEAD').toString().trim() execSync('git symbolic-ref --short -q HEAD || git rev-parse --short HEAD').toString().trim()
); );
} }
if (branches.length === 1) { const original_ref = execSync('git symbolic-ref --short -q HEAD || git rev-parse --short HEAD')
branches.push('main'); .toString()
.trim();
if (
requested_branches.length === 1 &&
!requested_branches.includes('main') &&
!fs.existsSync(`${outdir}/main.json`)
) {
requested_branches.push('main');
} }
process.on('exit', () => { process.on('exit', () => {
execSync(`git checkout ${branches[0]}`); execSync(`git checkout ${original_ref}`);
}); });
for (const branch of branches) { for (const branch of requested_branches) {
console.group(`Benchmarking ${branch}`); console.group(`Benchmarking ${branch}`);
const branch_profile_dir = `${PROFILE_DIR}/${safe(branch)}`;
if (fs.existsSync(branch_profile_dir))
fs.rmSync(branch_profile_dir, { recursive: true, force: true });
const branch_result_file = `${outdir}/${branch}.json`;
if (fs.existsSync(branch_result_file)) fs.rmSync(branch_result_file, { force: true });
execSync(`git checkout ${branch}`); execSync(`git checkout ${branch}`);
await new Promise((fulfil, reject) => { await new Promise((fulfil, reject) => {
const child = fork(runner); const child = fork(runner, [], {
env: {
...process.env,
BENCH_PROFILE_DIR: branch_profile_dir
}
});
child.on('message', (results) => { child.on('message', (results) => {
fs.writeFileSync(`${outdir}/${branch}.json`, JSON.stringify(results, null, ' ')); fs.writeFileSync(`${outdir}/${branch}.json`, JSON.stringify(results, null, ' '));
@ -58,33 +81,8 @@ for (const branch of branches) {
console.groupEnd(); console.groupEnd();
} }
const results = branches.map((branch) => { if (PROFILE_DIR !== null) {
return JSON.parse(fs.readFileSync(`${outdir}/${branch}.json`, 'utf-8')); console.log(`\nCPU profiles written to ${PROFILE_DIR}`);
});
for (let i = 0; i < results[0].length; i += 1) {
console.group(`${results[0][i].benchmark}`);
for (const metric of ['time', 'gc_time']) {
const times = results.map((result) => +result[i][metric]);
let min = Infinity;
let min_index = -1;
for (let b = 0; b < times.length; b += 1) {
if (times[b] < min) {
min = times[b];
min_index = b;
}
}
if (min !== 0) {
console.group(`${metric}: fastest is ${branches[min_index]}`);
times.forEach((time, b) => {
console.log(`${branches[b]}: ${time.toFixed(2)}ms (${((time / min) * 100).toFixed(2)}%)`);
});
console.groupEnd();
}
} }
console.groupEnd(); generate_report(outdir);
}

@ -0,0 +1,83 @@
import fs from 'node:fs';
import path from 'node:path';
const [benchmark, baseBranch = 'main', candidateBranch] = process.argv.slice(2);
if (!benchmark || !candidateBranch) {
console.error(
'Usage: node benchmarking/compare/profile-diff.mjs <benchmark> <base-branch> <candidate-branch>'
);
process.exit(1);
}
const root = path.resolve('benchmarking/compare/.profiles');
function safe(name) {
return name.replace(/[^a-z0-9._-]+/gi, '_');
}
function read_profile(branch, bench) {
const file = path.join(root, safe(branch), `${bench}.cpuprofile`);
const profile = JSON.parse(fs.readFileSync(file, 'utf8'));
const nodes = Array.isArray(profile.nodes) ? profile.nodes : [];
const samples = Array.isArray(profile.samples) ? profile.samples : [];
const id_to_node = new Map(nodes.map((node) => [node.id, node]));
const self_counts = new Map();
for (const sample of samples) {
if (typeof sample !== 'number') continue;
self_counts.set(sample, (self_counts.get(sample) ?? 0) + 1);
}
const total = samples.length || 1;
const by_fn = new Map();
for (const [id, count] of self_counts) {
const node = id_to_node.get(id);
if (!node || typeof node !== 'object') continue;
const frame = node.callFrame ?? {};
const function_name = frame.functionName || '(anonymous)';
const url = frame.url || '';
const line = typeof frame.lineNumber === 'number' ? frame.lineNumber + 1 : 0;
const label = url
? `${function_name} @ ${url.replace(/^.*packages\//, 'packages/')}:${line}`
: function_name;
by_fn.set(label, (by_fn.get(label) ?? 0) + count);
}
return { by_fn, total };
}
const base = read_profile(baseBranch, benchmark);
const candidate = read_profile(candidateBranch, benchmark);
const keys = new Set([...base.by_fn.keys(), ...candidate.by_fn.keys()]);
const rows = [...keys]
.map((key) => {
const base_pct = ((base.by_fn.get(key) ?? 0) * 100) / base.total;
const candidate_pct = ((candidate.by_fn.get(key) ?? 0) * 100) / candidate.total;
return {
key,
delta: candidate_pct - base_pct,
base_pct,
candidate_pct
};
})
.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta))
.slice(0, 20);
console.log(`Benchmark: ${benchmark}`);
console.log(`Base: ${baseBranch}`);
console.log(`Candidate: ${candidateBranch}`);
console.log('');
for (const row of rows) {
const sign = row.delta >= 0 ? '+' : '';
console.log(
`${sign}${row.delta.toFixed(2).padStart(6)}pp candidate ${row.candidate_pct.toFixed(2).padStart(6)}% base ${row.base_pct.toFixed(2).padStart(6)}% ${row.key}`
);
}

@ -1,10 +1,18 @@
import { benchmarks } from '../benchmarks.js'; import { reactivity_benchmarks } from '../benchmarks/reactivity/index.js';
import { with_cpu_profile } from '../utils.js';
const results = []; const results = [];
for (const benchmark of benchmarks) { const PROFILE_DIR = process.env.BENCH_PROFILE_DIR;
const result = await benchmark();
console.error(result.benchmark); for (let i = 0; i < reactivity_benchmarks.length; i += 1) {
results.push(result); const benchmark = reactivity_benchmarks[i];
process.stderr.write(`Running ${i + 1}/${reactivity_benchmarks.length} ${benchmark.label} `);
results.push({
benchmark: benchmark.label,
...(await with_cpu_profile(PROFILE_DIR, benchmark.label, () => benchmark.fn()))
});
process.stderr.write('\x1b[2K\r');
} }
process.send(results); process.send(results);

@ -1,55 +1,94 @@
import * as $ from '../packages/svelte/src/internal/client/index.js'; import * as $ from '../packages/svelte/src/internal/client/index.js';
import { reactivity_benchmarks } from './benchmarks/reactivity/index.js'; import { reactivity_benchmarks } from './benchmarks/reactivity/index.js';
import { ssr_benchmarks } from './benchmarks/ssr/index.js'; import { ssr_benchmarks } from './benchmarks/ssr/index.js';
import { with_cpu_profile } from './utils.js';
let total_time = 0; // e.g. `pnpm bench kairo` to only run the kairo benchmarks
let total_gc_time = 0; const filters = process.argv.slice(2);
const PROFILE_DIR = './benchmarking/.profiles';
const suites = [ const suites = [
{ benchmarks: reactivity_benchmarks, name: 'reactivity benchmarks' }, {
{ benchmarks: ssr_benchmarks, name: 'server-side rendering benchmarks' } benchmarks: reactivity_benchmarks.filter(
]; (b) => filters.length === 0 || filters.some((f) => b.label.includes(f))
),
name: 'reactivity benchmarks'
},
{
benchmarks: ssr_benchmarks.filter(
(b) => filters.length === 0 || filters.some((f) => b.label.includes(f))
),
name: 'server-side rendering benchmarks'
}
].filter((suite) => suite.benchmarks.length > 0);
if (suites.length === 0) {
console.log('No benchmarks matched provided filters');
process.exit(1);
}
const COLUMN_WIDTHS = [25, 9, 9];
const TOTAL_WIDTH = COLUMN_WIDTHS.reduce((a, b) => a + b);
const pad_right = (str, n) => str + ' '.repeat(n - str.length);
const pad_left = (str, n) => ' '.repeat(n - str.length) + str;
let total_time = 0;
let total_gc_time = 0;
// eslint-disable-next-line no-console
console.log('\x1b[1m', '-- Benchmarking Started --', '\x1b[0m');
$.push({}, true); $.push({}, true);
try { try {
for (const { benchmarks, name } of suites) { for (const { benchmarks, name } of suites) {
let suite_time = 0; let suite_time = 0;
let suite_gc_time = 0; let suite_gc_time = 0;
// eslint-disable-next-line no-console
console.log(`\nRunning ${name}...\n`); console.log(`\nRunning ${name}...\n`);
console.log(
pad_right('Benchmark', COLUMN_WIDTHS[0]) +
pad_left('Time', COLUMN_WIDTHS[1]) +
pad_left('GC time', COLUMN_WIDTHS[2])
);
console.log('='.repeat(TOTAL_WIDTH));
for (const benchmark of benchmarks) { for (const benchmark of benchmarks) {
const results = await benchmark(); const results = await with_cpu_profile(PROFILE_DIR, benchmark.label, () => benchmark.fn());
// eslint-disable-next-line no-console console.log(
console.log(results); pad_right(benchmark.label, COLUMN_WIDTHS[0]) +
total_time += Number(results.time); pad_left(results.time.toFixed(2), COLUMN_WIDTHS[1]) +
total_gc_time += Number(results.gc_time); pad_left(results.gc_time.toFixed(2), COLUMN_WIDTHS[2])
suite_time += Number(results.time); );
suite_gc_time += Number(results.gc_time); total_time += results.time;
total_gc_time += results.gc_time;
suite_time += results.time;
suite_gc_time += results.gc_time;
} }
console.log(`\nFinished ${name}.\n`); console.log('='.repeat(TOTAL_WIDTH));
console.log(
pad_right('suite', COLUMN_WIDTHS[0]) +
pad_left(suite_time.toFixed(2), COLUMN_WIDTHS[1]) +
pad_left(suite_gc_time.toFixed(2), COLUMN_WIDTHS[2])
);
console.log('='.repeat(TOTAL_WIDTH));
}
// eslint-disable-next-line no-console if (PROFILE_DIR !== null) {
console.log({ console.log(`\nCPU profiles written to ${PROFILE_DIR}`);
suite_time: suite_time.toFixed(2),
suite_gc_time: suite_gc_time.toFixed(2)
});
} }
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console
console.log('\x1b[1m', '\n-- Benchmarking Failed --\n', '\x1b[0m');
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(e); console.error(e);
process.exit(1); process.exit(1);
} }
$.pop(); $.pop();
// eslint-disable-next-line no-console
console.log('\x1b[1m', '\n-- Benchmarking Complete --\n', '\x1b[0m'); console.log('');
// eslint-disable-next-line no-console
console.log({ console.log(
total_time: total_time.toFixed(2), pad_right('total', COLUMN_WIDTHS[0]) +
total_gc_time: total_gc_time.toFixed(2) pad_left(total_time.toFixed(2), COLUMN_WIDTHS[1]) +
}); pad_left(total_gc_time.toFixed(2), COLUMN_WIDTHS[2])
);

@ -1,119 +1,333 @@
import { performance, PerformanceObserver } from 'node:perf_hooks'; import { performance, PerformanceObserver } from 'node:perf_hooks';
import fs from 'node:fs';
import path from 'node:path';
import inspector from 'node:inspector/promises';
import v8 from 'v8-natives'; import v8 from 'v8-natives';
import * as fs from 'node:fs';
import * as path from 'node:path';
// Credit to https://github.com/milomg/js-reactivity-benchmark for the logic for timing + GC tracking. // Credit to https://github.com/milomg/js-reactivity-benchmark for the logic for timing + GC tracking.
class GarbageTrack { async function track(fn) {
track_id = 0; v8.collectGarbage();
observer = new PerformanceObserver((list) => this.perf_entries.push(...list.getEntries()));
perf_entries = []; /** @type {PerformanceEntry[]} */
periods = []; const entries = [];
const observer = new PerformanceObserver((list) => entries.push(...list.getEntries()));
observer.observe({ entryTypes: ['gc'] });
watch(fn) {
this.track_id++;
const start = performance.now(); const start = performance.now();
const result = fn(); fn();
const end = performance.now(); const end = performance.now();
this.periods.push({ track_id: this.track_id, start, end });
return { result, track_id: this.track_id }; await new Promise((f) => setTimeout(f, 10));
const gc_time = entries
.filter((e) => e.startTime >= start && e.startTime < end)
.reduce((t, e) => e.duration + t, 0);
observer.disconnect();
return { time: end - start, gc_time };
} }
/** /**
* @param {number} track_id * @param {number} times
* @param {() => void} fn
*/ */
async gcDuration(track_id) { export async function fastest_test(times, fn) {
await promise_delay(10); /** @type {Array<{ time: number, gc_time: number }>} */
const results = [];
const period = this.periods.find((period) => period.track_id === track_id); for (let i = 0; i < times; i++) {
if (!period) { results.push(await track(fn));
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
return Promise.reject('no period found');
} }
const entries = this.perf_entries.filter( return results.reduce((a, b) => (a.time < b.time ? a : b));
(e) => e.startTime >= period.start && e.startTime < period.end }
);
return entries.reduce((t, e) => e.duration + t, 0); export function safe(name) {
return name.replace(/[^a-z0-9._-]+/gi, '_');
}
/**
* @param {unknown} value
*/
function format_markdown_value(value) {
if (value === null || value === undefined) return '';
if (Array.isArray(value)) return value.map((item) => String(item)).join(', ');
if (typeof value === 'object') return JSON.stringify(value);
return String(value);
} }
destroy() { /**
this.observer.disconnect(); * @param {string} text
*/
function escape_markdown_cell(text) {
return text.replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
} }
constructor() { /**
this.observer.observe({ entryTypes: ['gc'] }); * @param {string} value
*/
function normalize_profile_url(value) {
if (!value) return '';
if (value.startsWith('file://')) {
try {
const pathname = decodeURIComponent(new URL(value).pathname);
const relative = path.relative(process.cwd(), pathname);
if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) return relative;
return pathname;
} catch {
return value;
} }
} }
function promise_delay(timeout = 0) { if (path.isAbsolute(value)) {
return new Promise((resolve) => setTimeout(resolve, timeout)); const relative = path.relative(process.cwd(), value);
if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) return relative;
}
return value;
} }
/** /**
* @param {{ (): void; (): any; }} fn * @param {string} function_name
*/ */
function run_timed(fn) { function is_special_runtime_node(function_name) {
const start = performance.now(); return function_name === '(idle)' || function_name === '(garbage collector)';
const result = fn();
const time = performance.now() - start;
return { result, time };
} }
/** /**
* @param {() => void} fn * @param {string} normalized_url
*/ */
async function run_tracked(fn) { function is_svelte_source_url(normalized_url) {
v8.collectGarbage(); return normalized_url.startsWith('packages/svelte/');
const gc_track = new GarbageTrack();
const { result: wrappedResult, track_id } = gc_track.watch(() => run_timed(fn));
const gc_time = await gc_track.gcDuration(track_id);
const { result, time } = wrappedResult;
gc_track.destroy();
return { result, timing: { time, gc_time } };
} }
/** /**
* @param {number} times * @param {Record<string, unknown>} profile
* @param {() => void} fn
*/ */
export async function fastest_test(times, fn) { function profile_to_markdown(profile) {
const results = []; /** @type {string[]} */
for (let i = 0; i < times; i++) { const lines = ['# CPU profile'];
const run = await run_tracked(fn);
results.push(run); const metadata = Object.entries(profile).filter(
([key]) => key !== 'nodes' && key !== 'samples' && key !== 'timeDeltas'
);
if (metadata.length > 0) {
lines.push('', '## Metadata', '| Field | Value |', '| --- | --- |');
for (const [key, value] of metadata) {
lines.push(
`| ${escape_markdown_cell(key)} | ${escape_markdown_cell(format_markdown_value(value))} |`
);
}
} }
const fastest = results.reduce((a, b) => (a.timing.time < b.timing.time ? a : b));
return fastest; const nodes = Array.isArray(profile.nodes) ? profile.nodes : [];
const samples = Array.isArray(profile.samples) ? profile.samples : [];
const timeDeltas = Array.isArray(profile.timeDeltas) ? profile.timeDeltas : [];
/** @type {Set<number>} */
const included_node_ids = new Set();
if (nodes.length > 0) {
/** @type {Map<number, Record<string, unknown>>} */
const nodes_by_id = new Map();
/** @type {Map<number, number>} */
const parent_by_id = new Map();
for (const node of nodes) {
if (!node || typeof node !== 'object') continue;
if (typeof node.id !== 'number') continue;
nodes_by_id.set(node.id, node);
const children = Array.isArray(node.children) ? node.children : [];
for (const child of children) {
if (typeof child === 'number') {
parent_by_id.set(child, node.id);
}
} }
/** const callFrame =
* @param {boolean} a node.callFrame && typeof node.callFrame === 'object'
*/ ? /** @type {Record<string, unknown>} */ (node.callFrame)
export function assert(a) { : /** @type {Record<string, unknown>} */ ({});
if (!a) { const functionName =
throw new Error('Assertion failed'); typeof callFrame.functionName === 'string' ? callFrame.functionName : '(anonymous)';
const normalizedUrl =
typeof callFrame.url === 'string' ? normalize_profile_url(callFrame.url) : '';
if (is_special_runtime_node(functionName) || is_svelte_source_url(normalizedUrl)) {
included_node_ids.add(node.id);
} }
} }
/** /** @type {Map<number, number>} */
* @param {string} file const self_sample_count = new Map();
*/ for (const sample of samples) {
export function read_file(file) { if (typeof sample !== 'number') continue;
return fs.readFileSync(file, 'utf-8').replace(/\r\n/g, '\n'); if (!included_node_ids.has(sample)) continue;
self_sample_count.set(sample, (self_sample_count.get(sample) ?? 0) + 1);
}
/** @type {Map<number, number>} */
const inclusive_sample_count = new Map();
/** @type {Set<number>} */
const stack = new Set();
/** @param {number} node_id */
const get_inclusive_count = (node_id) => {
const cached = inclusive_sample_count.get(node_id);
if (cached !== undefined) return cached;
if (stack.has(node_id)) return self_sample_count.get(node_id) ?? 0;
stack.add(node_id);
const node = nodes_by_id.get(node_id);
const children = node && Array.isArray(node.children) ? node.children : [];
let total = self_sample_count.get(node_id) ?? 0;
for (const child of children) {
if (typeof child !== 'number') continue;
total += get_inclusive_count(child);
}
stack.delete(node_id);
inclusive_sample_count.set(node_id, total);
return total;
};
for (const node_id of included_node_ids) {
get_inclusive_count(node_id);
}
const total_samples = [...self_sample_count.values()].reduce((sum, count) => sum + count, 0);
if (total_samples > 0) {
const hotspot_rows = [...included_node_ids]
.map((id) => nodes_by_id.get(id))
.filter((node) => !!node)
.map((node) => {
const id = /** @type {number} */ (node.id);
const callFrame =
node.callFrame && typeof node.callFrame === 'object'
? /** @type {Record<string, unknown>} */ (node.callFrame)
: /** @type {Record<string, unknown>} */ ({});
const functionName =
typeof callFrame.functionName === 'string' && callFrame.functionName.length > 0
? callFrame.functionName
: '(anonymous)';
const selfCount = self_sample_count.get(id) ?? 0;
const inclusiveCount = inclusive_sample_count.get(id) ?? selfCount;
return { id, functionName, selfCount, inclusiveCount };
})
.filter((row) => row.selfCount > 0 || row.inclusiveCount > 0)
.sort(
(a, b) =>
b.inclusiveCount - a.inclusiveCount ||
b.selfCount - a.selfCount ||
String(a.id).localeCompare(String(b.id))
)
.slice(0, 25);
if (hotspot_rows.length > 0) {
lines.push(
'',
'## Top hotspots',
'| Rank | Node ID | Function | Self samples | Self % | Inclusive samples | Inclusive % |',
'| --- | --- | --- | --- | --- | --- | --- |'
);
for (let i = 0; i < hotspot_rows.length; i += 1) {
const row = hotspot_rows[i];
const selfPct = ((row.selfCount / total_samples) * 100).toFixed(2);
const inclusivePct = ((row.inclusiveCount / total_samples) * 100).toFixed(2);
lines.push(
`| ${i + 1} | ${row.id} | ${escape_markdown_cell(row.functionName)} | ${row.selfCount} | ${selfPct}% | ${row.inclusiveCount} | ${inclusivePct}% |`
);
}
}
}
lines.push(
'',
'## Nodes',
'| ID | Parent ID | Function | URL | Line | Column | Hit count | Children | Deopt reason |',
'| --- | --- | --- | --- | --- | --- | --- | --- | --- |'
);
for (const node of nodes) {
if (!node || typeof node !== 'object') continue;
if (typeof node.id !== 'number') continue;
if (!included_node_ids.has(node.id)) continue;
const callFrame =
node.callFrame && typeof node.callFrame === 'object'
? /** @type {Record<string, unknown>} */ (node.callFrame)
: /** @type {Record<string, unknown>} */ ({});
const id = typeof node.id === 'number' ? node.id : '';
const parentId =
typeof id === 'number' && included_node_ids.has(parent_by_id.get(id) ?? NaN)
? parent_by_id.get(id) ?? ''
: '';
const functionName =
typeof callFrame.functionName === 'string' && callFrame.functionName.length > 0
? callFrame.functionName
: '(anonymous)';
const url = typeof callFrame.url === 'string' ? normalize_profile_url(callFrame.url) : '';
const lineNumber =
typeof callFrame.lineNumber === 'number' ? String(callFrame.lineNumber + 1) : '';
const columnNumber =
typeof callFrame.columnNumber === 'number' ? String(callFrame.columnNumber + 1) : '';
const hitCount = typeof node.hitCount === 'number' ? node.hitCount : '';
const children = Array.isArray(node.children)
? node.children
.filter((child) => typeof child === 'number' && included_node_ids.has(child))
.join(', ')
: '';
const deoptReason = typeof node.deoptReason === 'string' ? node.deoptReason : '';
lines.push(
`| ${escape_markdown_cell(String(id))} | ${escape_markdown_cell(String(parentId))} | ${escape_markdown_cell(functionName)} | ${escape_markdown_cell(url)} | ${escape_markdown_cell(lineNumber)} | ${escape_markdown_cell(columnNumber)} | ${escape_markdown_cell(String(hitCount))} | ${escape_markdown_cell(children)} | ${escape_markdown_cell(deoptReason)} |`
);
}
}
return `${lines.join('\n')}\n`;
} }
/** /**
* @param {string} file * @template T
* @param {string} contents * @param {string | null} profile_dir
* @param {string} profile_name
* @param {() => T | Promise<T>} fn
* @returns {Promise<T>}
*/ */
export function write(file, contents) { export async function with_cpu_profile(profile_dir, profile_name, fn) {
try { if (profile_dir === null) {
fs.mkdirSync(path.dirname(file), { recursive: true }); return await fn();
} catch {} }
fs.mkdirSync(profile_dir, { recursive: true });
fs.writeFileSync(file, contents); const session = new inspector.Session();
session.connect();
await session.post('Profiler.enable');
await session.post('Profiler.start');
try {
return await fn();
} finally {
const { profile } = /** @type {{ profile: object }} */ (await session.post('Profiler.stop'));
const safe_profile_name = safe(profile_name);
const profile_file = path.join(profile_dir, `${safe_profile_name}.cpuprofile`);
const markdown_file = path.join(profile_dir, `${safe_profile_name}.md`);
fs.writeFileSync(profile_file, JSON.stringify(profile));
fs.writeFileSync(
markdown_file,
profile_to_markdown(/** @type {Record<string, unknown>} */ (profile))
);
session.disconnect();
}
} }

@ -2,9 +2,9 @@
title: Getting started title: Getting started
--- ---
We recommend using [SvelteKit](../kit), the official application framework from the Svelte team powered by [Vite](https://vite.dev/): We recommend using [SvelteKit](../kit), which lets you [build almost anything](../kit/project-types). It's the official application framework from the Svelte team and powered by [Vite](https://vite.dev/). Create a new project with:
```bash ```sh
npx sv create myapp npx sv create myapp
cd myapp cd myapp
npm install npm install
@ -15,15 +15,18 @@ Don't worry if you don't know Svelte yet! You can ignore all the nice features S
## Alternatives to SvelteKit ## Alternatives to SvelteKit
You can also use Svelte directly with Vite by running `npm create vite@latest` and selecting the `svelte` option. With this, `npm run build` will generate HTML, JS and CSS files inside the `dist` directory using [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte). In most cases, you will probably need to [choose a routing library](faq#Is-there-a-router) as well. You can also use Svelte directly with Vite via [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte) by running `npm create vite@latest` and selecting the `svelte` option (or, if working with an existing project, adding the plugin to your `vite.config.js` file). With this, `npm run build` will generate HTML, JS, and CSS files inside the `dist` directory. In most cases, you will probably need to [choose a routing library](/packages#routing) as well.
There are also plugins for [Rollup](https://github.com/sveltejs/rollup-plugin-svelte), [Webpack](https://github.com/sveltejs/svelte-loader) [and a few others](https://sveltesociety.dev/packages?category=build-plugins), but we recommend Vite. >[!NOTE] Vite is often used in standalone mode to build [single page apps (SPAs)](../kit/glossary#SPA), which you can also [build with SvelteKit](../kit/single-page-apps).
There are also [plugins for other bundlers](/packages#bundler-plugins), but we recommend Vite.
## Editor tooling ## Editor tooling
The Svelte team maintains a [VS Code extension](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode), and there are integrations with various other [editors](https://sveltesociety.dev/resources#editor-support) and tools as well. The Svelte team maintains a [VS Code extension](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode), and there are integrations with various other [editors](https://sveltesociety.dev/collection/editor-support-c85c080efc292a34) and tools as well.
You can also check your code from the command line using [`npx sv check`](https://svelte.dev/docs/cli/sv-check).
You can also check your code from the command line using [sv check](https://github.com/sveltejs/cli).
## Getting help ## Getting help

@ -4,7 +4,7 @@ title: .svelte.js and .svelte.ts files
Besides `.svelte` files, Svelte also operates on `.svelte.js` and `.svelte.ts` files. Besides `.svelte` files, Svelte also operates on `.svelte.js` and `.svelte.ts` files.
These behave like any other `.js` or `.ts` module, except that you can use runes. This is useful for creating reusable reactive logic, or sharing reactive state across your app. These behave like any other `.js` or `.ts` module, except that you can use runes. This is useful for creating reusable reactive logic, or sharing reactive state across your app (though note that you [cannot export reassigned state]($state#Passing-state-across-modules)).
> [!LEGACY] > [!LEGACY]
> This is a concept that didn't exist prior to Svelte 5 > This is a concept that didn't exist prior to Svelte 5

@ -1,139 +0,0 @@
---
title: Public API of a component
---
### Public API of a component
Svelte uses the `$props` rune to declare _properties_ or _props_, which means describing the public interface of the component which becomes accessible to consumers of the component.
> [!NOTE] `$props` is one of several runes, which are special hints for Svelte's compiler to make things reactive.
```svelte
<script>
let { foo, bar, baz } = $props();
// Values that are passed in as props
// are immediately available
console.log({ foo, bar, baz });
</script>
```
You can specify a fallback value for a prop. It will be used if the component's consumer doesn't specify the prop on the component when instantiating the component, or if the passed value is `undefined` at some point.
```svelte
<script>
let { foo = 'optional default initial value' } = $props();
</script>
```
To get all properties, use rest syntax:
```svelte
<script>
let { a, b, c, ...everythingElse } = $props();
</script>
```
You can use reserved words as prop names.
```svelte
<script>
// creates a `class` property, even
// though it is a reserved word
let { class: className } = $props();
</script>
```
If you're using TypeScript, you can declare the prop types:
```svelte
<script lang="ts">
interface Props {
required: string;
optional?: number;
[key: string]: unknown;
}
let { required, optional, ...everythingElse }: Props = $props();
</script>
```
If you're using JavaScript, you can declare the prop types using JSDoc:
```svelte
<script>
/** @type {{ x: string }} */
let { x } = $props();
// or use @typedef if you want to document the properties:
/**
* @typedef {Object} MyProps
* @property {string} y Some documentation
*/
/** @type {MyProps} */
let { y } = $props();
</script>
```
If you export a `const`, `class` or `function`, it is readonly from outside the component.
```svelte
<script>
export const thisIs = 'readonly';
export function greet(name) {
alert(`hello ${name}!`);
}
</script>
```
Readonly props can be accessed as properties on the element, tied to the component using [`bind:this` syntax](bindings#bind:this).
### Reactive variables
To change component state and trigger a re-render, just assign to a locally declared variable that was declared using the `$state` rune.
Update expressions (`count += 1`) and property assignments (`obj.x = y`) have the same effect.
```svelte
<script>
let count = $state(0);
function handleClick() {
// calling this function will trigger an
// update if the markup references `count`
count = count + 1;
}
</script>
```
Svelte's `<script>` blocks are run only when the component is created, so assignments within a `<script>` block are not automatically run again when a prop updates.
```svelte
<script>
let { person } = $props();
// this will only set `name` on component creation
// it will not update when `person` does
let { name } = person;
</script>
```
If you'd like to react to changes to a prop, use the `$derived` or `$effect` runes instead.
```svelte
<script>
let count = $state(0);
let double = $derived(count * 2);
$effect(() => {
if (count > 10) {
alert('Too high!');
}
});
</script>
```
For more information on reactivity, read the documentation around runes.

@ -1,144 +0,0 @@
---
title: Reactivity fundamentals
---
Reactivity is at the heart of interactive UIs. When you click a button, you expect some kind of response. It's your job as a developer to make this happen. It's Svelte's job to make your job as intuitive as possible, by providing a good API to express reactive systems.
## Runes
Svelte 5 uses _runes_, a powerful set of primitives for controlling reactivity inside your Svelte components and inside `.svelte.js` and `.svelte.ts` modules.
Runes are function-like symbols that provide instructions to the Svelte compiler. You don't need to import them from anywhere — when you use Svelte, they're part of the language.
The following sections introduce the most important runes for declare state, derived state and side effects at a high level. For more details refer to the later sections on [state](state) and [side effects](side-effects).
## `$state`
Reactive state is declared with the `$state` rune:
```svelte
<script>
let count = $state(0);
</script>
<button onclick={() => count++}>
clicks: {count}
</button>
```
You can also use `$state` in class fields (whether public or private):
```js
// @errors: 7006 2554
class Todo {
done = $state(false);
text = $state();
constructor(text) {
this.text = text;
}
}
```
> [!LEGACY]
> In Svelte 4, state was implicitly reactive if the variable was declared at the top level
>
> ```svelte
> <script>
> let count = 0;
> </script>
>
> <button on:click={() => count++}>
> clicks: {count}
> </button>
> ```
## `$derived`
Derived state is declared with the `$derived` rune:
```svelte
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>
<button onclick={() => count++}>
{doubled}
</button>
<p>{count} doubled is {doubled}</p>
```
The expression inside `$derived(...)` should be free of side-effects. Svelte will disallow state changes (e.g. `count++`) inside derived expressions.
As with `$state`, you can mark class fields as `$derived`.
> [!LEGACY]
> In Svelte 4, you could use reactive statements for this.
>
> ```svelte
> <script>
> let count = 0;
> $: doubled = count * 2;
> </script>
>
> <button on:click={() => count++}>
> {doubled}
> </button>
>
> <p>{count} doubled is {doubled}</p>
> ```
>
> This only worked at the top level of a component.
## `$effect`
To run _side-effects_ when the component is mounted to the DOM, and when values change, we can use the `$effect` rune ([demo](/playground/untitled#H4sIAAAAAAAAE31T24rbMBD9lUG7kAQ2sbdlX7xOYNk_aB_rQhRpbAsU2UiTW0P-vbrYubSlYGzmzMzROTPymdVKo2PFjzMzfIusYB99z14YnfoQuD1qQh-7bmdFQEonrOppVZmKNBI49QthCc-OOOH0LZ-9jxnR6c7eUpOnuv6KeT5JFdcqbvbcBcgDz1jXKGg6ncFyBedYR6IzLrAZwiN5vtSxaJA-EzadfJEjKw11C6GR22-BLH8B_wxdByWpvUYtqqal2XB6RVkG1CoHB6U1WJzbnYFDiwb3aGEdDa3Bm1oH12sQLTcNPp7r56m_00mHocSG97_zd7ICUXonA5fwKbPbkE2ZtMJGGVkEdctzQi4QzSwr9prnFYNk5hpmqVuqPQjNnfOJoMF22lUsrq_UfIN6lfSVyvQ7grB3X2mjMZYO3XO9w-U5iLx42qg29md3BP_ni5P4gy9ikTBlHxjLzAtPDlyYZmRdjAbGq7HprEQ7p64v4LU_guu0kvAkhBim3nMplWl8FreQD-CW20aZR0wq12t-KqDWeBywhvexKC3memmDwlHAv9q4Vo2ZK8KtK0CgX7u9J8wXbzdKv-nRnfF_2baTqlYoWUF2h5efl9-n0O6koAMAAA==)):
```svelte
<script>
let size = $state(50);
let color = $state('#ff3e00');
let canvas;
$effect(() => {
const context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
// this will re-run whenever `color` or `size` change
context.fillStyle = color;
context.fillRect(0, 0, size, size);
});
</script>
<canvas bind:this={canvas} width="100" height="100" />
```
The function passed to `$effect` will run when the component mounts, and will re-run after any changes to the values it reads that were declared with `$state` or `$derived` (including those passed in with `$props`). Re-runs are batched (i.e. changing `color` and `size` in the same moment won't cause two separate runs), and happen after any DOM updates have been applied.
> [!LEGACY]
> In Svelte 4, you could use reactive statements for this.
>
> ```svelte
> <script>
> let size = 50;
> let color = '#ff3e00';
>
> let canvas;
>
> $: {
> const context = canvas.getContext('2d');
> context.clearRect(0, 0, canvas.width, canvas.height);
>
> // this will re-run whenever `color` or `size` change
> context.fillStyle = color;
> context.fillRect(0, 0, size, size);
> }
> </script>
>
> <canvas bind:this={canvas} width="100" height="100" />
> ```
>
> This only worked at the top level of a component.

@ -2,7 +2,7 @@
title: What are runes? title: What are runes?
--- ---
> [!NOTE] **rune** /ro͞on/ _noun_ > [!NOTE] **rune** /ruːn/ _noun_
> >
> A letter or mark used as a mystical or magic symbol. > A letter or mark used as a mystical or magic symbol.

@ -1,5 +1,6 @@
--- ---
title: $state title: $state
tags: rune-state
--- ---
The `$state` rune allows you to create _reactive state_, which means that your UI _reacts_ when it changes. The `$state` rune allows you to create _reactive state_, which means that your UI _reacts_ when it changes.
@ -20,9 +21,7 @@ Unlike other frameworks you may have encountered, there is no API for interactin
If `$state` is used with an array or a simple object, the result is a deeply reactive _state proxy_. [Proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) allow Svelte to run code when you read or write properties, including via methods like `array.push(...)`, triggering granular updates. If `$state` is used with an array or a simple object, the result is a deeply reactive _state proxy_. [Proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) allow Svelte to run code when you read or write properties, including via methods like `array.push(...)`, triggering granular updates.
> [!NOTE] Classes like `Set` and `Map` will not be proxied, but Svelte provides reactive implementations for various built-ins like these that can be imported from [`svelte/reactivity`](./svelte-reactivity). State is proxified recursively until Svelte finds something other than an array or simple object (like a class or an object created with `Object.create`). In a case like this...
State is proxified recursively until Svelte finds something other than an array or simple object. In a case like this...
```js ```js
let todos = $state([ let todos = $state([
@ -44,12 +43,7 @@ todos[0].done = !todos[0].done;
If you push a new object to the array, it will also be proxified: If you push a new object to the array, it will also be proxified:
```js ```js
// @filename: ambient.d.ts let todos = [{ done: false, text: 'add more todos' }];
declare global {
const todos: Array<{ done: boolean, text: string }>
}
// @filename: index.js
// ---cut--- // ---cut---
todos.push({ todos.push({
done: false, done: false,
@ -57,7 +51,7 @@ todos.push({
}); });
``` ```
> [!NOTE] When you update properties of proxies, the original object is _not_ mutated. > [!NOTE] When you update properties of proxies, the original object is _not_ mutated. If you need to use your own proxy handlers in a state proxy, [you should wrap the object _after_ wrapping it in `$state`](https://svelte.dev/playground/hello-world?version=latest#H4sIAAAAAAAACpWR3WoDIRCFX2UqhWyIJL3erAulL9C7XnQLMe5ksbUqOpsfln33YuyGFNJC8UKdc2bOhw7Myk9kJXsJ0nttO9jcR5KEG9AWJDwHdzwxznbaYGTl68Do5JM_FRifuh-9X8Y9Gkq1rYx4q66cJbQUWcmqqIL2VDe2IYMEbvuOikBADi-GJDSkXG-phId0G-frye2DO2psQYDFQ0Ys8gQO350dUkEydEg82T0GOs0nsSG9g2IqgxACZueo2ZUlpdvoDC6N64qsg1QKY8T2bpZp8gpIfbCQ85Zn50Ud82HkeY83uDjspenxv3jXcSDyjPWf9L1vJf0GH666J-jLu1ery4dV257IWXBWGa0-xFDMQdTTn2ScxWKsn86ROsLwQxqrVR5QM84Ij8TKFD2-cUZSm4O2LSt30kQcvwCgCmfZnAIAAA==).
Note that if you destructure a reactive value, the references are not reactive — as in normal JavaScript, they are evaluated at the point of destructuring: Note that if you destructure a reactive value, the references are not reactive — as in normal JavaScript, they are evaluated at the point of destructuring:
@ -72,16 +66,15 @@ todos[0].done = !todos[0].done;
### Classes ### Classes
You can also use `$state` in class fields (whether public or private): Class instances are not proxied. Instead, you can use `$state` in class fields (whether public or private), or as the first assignment to a property immediately inside the `constructor`:
```js ```js
// @errors: 7006 2554 // @errors: 7006 2554
class Todo { class Todo {
done = $state(false); done = $state(false);
text = $state();
constructor(text) { constructor(text) {
this.text = text; this.text = $state(text);
} }
reset() { reset() {
@ -115,10 +108,9 @@ You can either use an inline function...
// @errors: 7006 2554 // @errors: 7006 2554
class Todo { class Todo {
done = $state(false); done = $state(false);
text = $state();
constructor(text) { constructor(text) {
this.text = text; this.text = $state(text);
} }
+++reset = () => {+++ +++reset = () => {+++
@ -128,6 +120,10 @@ class Todo {
} }
``` ```
### Built-in classes
Svelte provides reactive implementations of built-in classes like `Set`, `Map`, `Date` and `URL` that can be imported from [`svelte/reactivity`](svelte-reactivity).
## `$state.raw` ## `$state.raw`
In cases where you don't want objects and arrays to be deeply reactive you can use `$state.raw`. In cases where you don't want objects and arrays to be deeply reactive you can use `$state.raw`.
@ -152,6 +148,8 @@ person = {
This can improve performance with large arrays and objects that you weren't planning to mutate anyway, since it avoids the cost of making them reactive. Note that raw state can _contain_ reactive state (for example, a raw array of reactive objects). This can improve performance with large arrays and objects that you weren't planning to mutate anyway, since it avoids the cost of making them reactive. Note that raw state can _contain_ reactive state (for example, a raw array of reactive objects).
As with `$state`, you can declare class fields using `$state.raw`.
## `$state.snapshot` ## `$state.snapshot`
To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`: To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:
@ -169,6 +167,23 @@ To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snaps
This is handy when you want to pass some state to an external library or API that doesn't expect a proxy, such as `structuredClone`. This is handy when you want to pass some state to an external library or API that doesn't expect a proxy, such as `structuredClone`.
If a value has a `toJSON` method, the snapshot will clone the value returned from `toJSON` instead of the original object.
## `$state.eager`
When state changes, it may not be reflected in the UI immediately if it is used by an `await` expression, because [updates are synchronized](await-expressions#Synchronized-updates).
In some cases, you may want to update the UI as soon as the state changes. For example, you might want to update a navigation bar when the user clicks on a link, so that they get visual feedback while waiting for the new page to load. To do this, use `$state.eager(value)`:
```svelte
<nav>
<a href="/" aria-current={$state.eager(pathname) === '/' ? 'page' : null}>home</a>
<a href="/about" aria-current={$state.eager(pathname) === '/about' ? 'page' : null}>about</a>
</nav>
```
Use this feature sparingly, and only to provide feedback in response to user action — in general, allowing Svelte to coordinate updates will provide a better user experience.
## Passing state into functions ## Passing state into functions
JavaScript is a _pass-by-value_ language — when you call a function, the arguments are the _values_ rather than the _variables_. In other words: JavaScript is a _pass-by-value_ language — when you call a function, the arguments are the _values_ rather than the _variables_. In other words:
@ -255,3 +270,83 @@ console.log(total.value); // 7
``` ```
...though if you find yourself writing code like that, consider using [classes](#Classes) instead. ...though if you find yourself writing code like that, consider using [classes](#Classes) instead.
## Passing state across modules
You can declare state in `.svelte.js` and `.svelte.ts` files, but you can only _export_ that state if it's not directly reassigned. In other words you can't do this:
```js
/// file: state.svelte.js
export let count = $state(0);
export function increment() {
count += 1;
}
```
That's because every reference to `count` is transformed by the Svelte compiler — the code above is roughly equivalent to this:
```js
/// file: state.svelte.js (compiler output)
// @filename: index.ts
interface Signal<T> {
value: T;
}
interface Svelte {
state<T>(value?: T): Signal<T>;
get<T>(source: Signal<T>): T;
set<T>(source: Signal<T>, value: T): void;
}
declare const $: Svelte;
// ---cut---
export let count = $.state(0);
export function increment() {
$.set(count, $.get(count) + 1);
}
```
> [!NOTE] You can see the code Svelte generates by clicking the 'JS Output' tab in the [playground](/playground).
Since the compiler only operates on one file at a time, if another file imports `count` Svelte doesn't know that it needs to wrap each reference in `$.get` and `$.set`:
```js
// @filename: state.svelte.js
export let count = 0;
// @filename: index.js
// ---cut---
import { count } from './state.svelte.js';
console.log(typeof count); // 'object', not 'number'
```
This leaves you with two options for sharing state between modules — either don't reassign it...
```js
// This is allowed — since we're updating
// `counter.count` rather than `counter`,
// Svelte doesn't wrap it in `$.state`
export const counter = $state({
count: 0
});
export function increment() {
counter.count += 1;
}
```
...or don't directly export it:
```js
let count = $state(0);
export function getCount() {
return count;
}
export function increment() {
count += 1;
}
```

@ -1,5 +1,6 @@
--- ---
title: $derived title: $derived
tags: rune-derived
--- ---
Derived state is declared with the `$derived` rune: Derived state is declared with the `$derived` rune:
@ -50,4 +51,96 @@ In essence, `$derived(expression)` is equivalent to `$derived.by(() => expressio
Anything read synchronously inside the `$derived` expression (or `$derived.by` function body) is considered a _dependency_ of the derived state. When the state changes, the derived will be marked as _dirty_ and recalculated when it is next read. Anything read synchronously inside the `$derived` expression (or `$derived.by` function body) is considered a _dependency_ of the derived state. When the state changes, the derived will be marked as _dirty_ and recalculated when it is next read.
In addition, if an expression contains an [`await`](await-expressions), Svelte transforms it such that any state _after_ the `await` is also tracked — in other words, in a case like this...
```js
let a = Promise.resolve(1);
let b = 2;
// ---cut---
let total = $derived(await a + b);
```
...both `a` and `b` are tracked, even though `b` is only read once `a` has resolved, after the initial execution. (This does not apply to `await` in functions that are called by the expression, only the expression itself.)
To exempt a piece of state from being treated as a dependency, use [`untrack`](svelte#untrack). To exempt a piece of state from being treated as a dependency, use [`untrack`](svelte#untrack).
## Overriding derived values
Derived expressions are recalculated when their dependencies change, but you can temporarily override their values by reassigning them (unless they are declared with `const`). This can be useful for things like _optimistic UI_, where a value is derived from the 'source of truth' (such as data from your server) but you'd like to show immediate feedback to the user:
```svelte
<script>
let { post, like } = $props();
let likes = $derived(post.likes);
async function onclick() {
// increment the `likes` count immediately...
likes += 1;
// and tell the server, which will eventually update `post`
try {
await like();
} catch {
// failed! roll back the change
likes -= 1;
}
}
</script>
<button {onclick}>🧡 {likes}</button>
```
> [!NOTE] Prior to Svelte 5.25, deriveds were read-only.
## Deriveds and reactivity
Unlike `$state`, which converts objects and arrays to [deeply reactive proxies]($state#Deep-state), `$derived` values are left as-is. For example, [in a case like this](/playground/untitled#H4sIAAAAAAAAE4VU22rjMBD9lUHd3aaQi9PdstS1A3t5XvpQ2Ic4D7I1iUUV2UjjNMX431eS7TRdSosxgjMzZ45mjt0yzffIYibvy0ojFJWqDKCQVBk2ZVup0LJ43TJ6rn2aBxw-FP2o67k9oCKP5dziW3hRaUJNjoYltjCyplWmM1JIIAn3FlL4ZIkTTtYez6jtj4w8WwyXv9GiIXiQxLVs9pfTMR7EuoSLIuLFbX7Z4930bZo_nBrD1bs834tlfvsBz9_SyX6PZXu9XaL4gOWn4sXjeyzftv4ZWfyxubpzxzg6LfD4MrooxELEosKCUPigQCMPKCZh0OtQE1iSxcsmdHuBvCiHZXALLXiN08EL3RRkaJ_kDVGle0HcSD5TPEeVtj67O4Nrg9aiSNtBY5oODJkrL5QsHtN2cgXp6nSJMWzpWWGasdlsGEMbzi5jPr5KFr0Ep7pdeM2-TCelCddIhDxAobi1jqF3cMaC1RKp64bAW9iFAmXGIHfd4wNXDabtOLN53w8W53VvJoZLh7xk4Rr3CoL-UNoLhWHrT1JQGcM17u96oES5K-kc2XOzkzqGCKL5De79OUTyyrg1zgwXsrEx3ESfx4Bz0M5UjVMHB24mw9SuXtXFoN13fYKOM1tyUT3FbvbWmSWCZX2Er-41u5xPoml45svRahl9Wb9aasbINJixDZwcPTbyTLZSUsAvrg_cPuCR7s782_WU8343Y72Qtlb8OYatwuOQvuN13M_hJKNfxann1v1U_B1KZ_D_mzhzhz24fw85CSz2irtN9w9HshBK7AQAAA==)...
```js
// @errors: 7005
let items = $state([ /*...*/ ]);
let index = $state(0);
let selected = $derived(items[index]);
```
...you can change (or `bind:` to) properties of `selected` and it will affect the underlying `items` array. If `items` was _not_ deeply reactive, mutating `selected` would have no effect.
## Destructuring
If you use destructuring with a `$derived` declaration, the resulting variables will all be reactive — this...
```js
function stuff() { return { a: 1, b: 2, c: 3 } }
// ---cut---
let { a, b, c } = $derived(stuff());
```
...is roughly equivalent to this:
```js
function stuff() { return { a: 1, b: 2, c: 3 } }
// ---cut---
let _stuff = $derived(stuff());
let a = $derived(_stuff.a);
let b = $derived(_stuff.b);
let c = $derived(_stuff.c);
```
## Update propagation
Svelte uses something called _push-pull reactivity_ — when state is updated, everything that depends on the state (whether directly or indirectly) is immediately notified of the change (the 'push'), but derived values are not re-evaluated until they are actually read (the 'pull').
If the new value of a derived is referentially identical to its previous value, downstream updates will be skipped. In other words, Svelte will only update the text inside the button when `large` changes, not when `count` changes, even though `large` depends on `count`:
```svelte
<script>
let count = $state(0);
let large = $derived(count > 10);
</script>
<button onclick={() => count++}>
{large}
</button>
```

@ -1,16 +1,13 @@
--- ---
title: $effect title: $effect
tags: rune-effect
--- ---
Effects are what make your application _do things_. When Svelte runs an effect function, it tracks which pieces of state (and derived state) are accessed (unless accessed inside [`untrack`](svelte#untrack)), and re-runs the function when that state later changes. Effects are functions that run when state updates, and can be used for things like calling third-party libraries, drawing on `<canvas>` elements, or making network requests. They only run in the browser, not during server-side rendering.
Most of the effects in a Svelte app are created by Svelte itself — they're the bits that update the text in `<h1>hello {name}!</h1>` when `name` changes, for example. Generally speaking, you should _not_ update state inside effects, as it will make code more convoluted and will often lead to never-ending update cycles. If you find yourself doing so, see [when not to use `$effect`](#When-not-to-use-$effect) to learn about alternative approaches.
But you can also create your own effects with the `$effect` rune, which is useful when you need to synchronize an external system (whether that's a library, or a `<canvas>` element, or something across a network) with state inside your Svelte app. You can create an effect with the `$effect` rune ([demo](/playground/untitled#H4sIAAAAAAAAE31S246bMBD9lZF3pSRSAqTVvrCAVPUP2sdSKY4ZwJJjkD0hSVH-vbINuWxXfQH5zMyZc2ZmZLVUaFn6a2R06ZGlHmBrpvnBvb71fWQHVOSwPbf4GS46TajJspRlVhjZU1HqkhQSWPkHIYdXS5xw-Zas3ueI6FRn7qHFS11_xSRZhIxbFtcDtw7SJb1iXaOg5XIFeQGjzyPRaevYNOGZIJ8qogbpe8CWiy_VzEpTXiQUcvPDkSVrSNZz1UlW1N5eLcqmpdXUvaQ4BmqlhZNUCgxuzFHDqUWNAxrYeUM76AzsnOsdiJbrBp_71lKpn3RRbii-4P3f-IMsRxS-wcDV_bL4PmSdBa2wl7pKnbp8DMgVvJm8ZNskKRkEM_OzyOKQFkgqOYBQ3Nq89Ns0nbIl81vMFN-jKoLMTOr-SOBOJS-Z8f5Y6D1wdcR8dFqvEBdetK-PHwj-z-cH8oHPY54wRJ8Ys7iSQ3Bg3VA9azQbmC9k35kKzYa6PoVtfwbbKVnBixBiGn7Pq0rqJoUtHiCZwAM3jdTPWCVtr_glhVrhecIa3vuksJ_b7TqFs4DPyriSjd5IwoNNQaAmNI-ESfR2p8zimzvN1swdCkvJHPH6-_oX8o1SgcIDAAA=)):
> [!NOTE] Avoid overusing `$effect`! When you do too much work in effects, code often becomes difficult to understand and maintain. See [when not to use `$effect`](#When-not-to-use-$effect) to learn about alternative approaches.
Your effects run after the component has been mounted to the DOM, and in a [microtask](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide) after state changes ([demo](/playground/untitled#H4sIAAAAAAAAE31S246bMBD9lZF3pSRSAqTVvrCAVPUP2sdSKY4ZwJJjkD0hSVH-vbINuWxXfQH5zMyZc2ZmZLVUaFn6a2R06ZGlHmBrpvnBvb71fWQHVOSwPbf4GS46TajJspRlVhjZU1HqkhQSWPkHIYdXS5xw-Zas3ueI6FRn7qHFS11_xSRZhIxbFtcDtw7SJb1iXaOg5XIFeQGjzyPRaevYNOGZIJ8qogbpe8CWiy_VzEpTXiQUcvPDkSVrSNZz1UlW1N5eLcqmpdXUvaQ4BmqlhZNUCgxuzFHDqUWNAxrYeUM76AzsnOsdiJbrBp_71lKpn3RRbii-4P3f-IMsRxS-wcDV_bL4PmSdBa2wl7pKnbp8DMgVvJm8ZNskKRkEM_OzyOKQFkgqOYBQ3Nq89Ns0nbIl81vMFN-jKoLMTOr-SOBOJS-Z8f5Y6D1wdcR8dFqvEBdetK-PHwj-z-cH8oHPY54wRJ8Ys7iSQ3Bg3VA9azQbmC9k35kKzYa6PoVtfwbbKVnBixBiGn7Pq0rqJoUtHiCZwAM3jdTPWCVtr_glhVrhecIa3vuksJ_b7TqFs4DPyriSjd5IwoNNQaAmNI-ESfR2p8zimzvN1swdCkvJHPH6-_oX8o1SgcIDAAA=)):
```svelte ```svelte
<script> <script>
@ -29,16 +26,26 @@ Your effects run after the component has been mounted to the DOM, and in a [micr
}); });
</script> </script>
<canvas bind:this={canvas} width="100" height="100" /> <canvas bind:this={canvas} width="100" height="100"></canvas>
``` ```
Re-runs are batched (i.e. changing `color` and `size` in the same moment won't cause two separate runs), and happen after any DOM updates have been applied. When Svelte runs an effect function, it tracks which pieces of state (and derived state) are accessed (unless accessed inside [`untrack`](svelte#untrack)), and re-runs the function when that state later changes.
> [!NOTE] If you're having difficulty understanding why your `$effect` is rerunning or is not running see [understanding dependencies](#Understanding-dependencies). Effects are triggered differently than the `$:` blocks you may be used to if coming from Svelte 4.
### Understanding lifecycle
Your effects run after the component has been mounted to the DOM, and in a [microtask](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide) after state changes. Re-runs are batched (i.e. changing `color` and `size` in the same moment won't cause two separate runs), and happen after any DOM updates have been applied.
You can place `$effect` anywhere, not just at the top level of a component, as long as it is called during component initialization (or while a parent effect is active). It is then tied to the lifecycle of the component (or parent effect) and will therefore destroy itself when the component unmounts (or the parent effect is destroyed). You can use `$effect` anywhere, not just at the top level of a component, as long as it is called while a parent effect is running.
You can return a function from `$effect`, which will run immediately before the effect re-runs, and before it is destroyed ([demo](/playground/untitled#H4sIAAAAAAAAE42RQY-bMBCF_8rI2kPopiXpMQtIPfbeW6m0xjyKtWaM7CFphPjvFVB2k2oPe7LmzXzyezOjaqxDVKefo5JrD3VaBLVXrLu5-tb3X-IZTmat0hHv6cazgCWqk8qiCbaXouRSHISMH1gop4coWrA7JE9bp7PO2QjjuY5vA8fDYZ3hUh7QNDCy2yWUFzTOUilpSj9aG-linaMKFGACtKCmSwvGGYGeLQvCWbtnMq3m34grajxHoa1JOUXI93_V_Sfz7Oz7Mafj0ypN-zvHm8dSAmQITP_xaUq2IU1GO1dp80I2Uh_82dao92Rl9R8GvgF0QrbrUFstcFeq0PgAkha0LoICPoeB4w1SJUvsZcj4rvcMlvmvGlGCv6J-DeSgw2vabQnJlm55p7nM0rcTctYei3HZxZSl7XHVqkHEM3k2zpqXfFyj393zU05fpyI6f0HI0hUoPoamC9roKDeo2ivBH1EnCQOmX9NfYw2GHrgCAAA=)). > [!NOTE] Svelte uses effects internally to represent logic and expressions in your template — this is how `<h1>hello {name}!</h1>` updates when `name` changes.
An effect can return a _teardown function_ which will run immediately before the effect re-runs:
<!-- codeblock:start {"title":"Effect teardown"} -->
```svelte ```svelte
<!--- file: App.svelte --->
<script> <script>
let count = $state(0); let count = $state(0);
let milliseconds = $state(1000); let milliseconds = $state(1000);
@ -50,7 +57,7 @@ You can return a function from `$effect`, which will run immediately before the
}, milliseconds); }, milliseconds);
return () => { return () => {
// if a callback is provided, it will run // if a teardown function is provided, it will run
// a) immediately before the effect re-runs // a) immediately before the effect re-runs
// b) when the component is destroyed // b) when the component is destroyed
clearInterval(interval); clearInterval(interval);
@ -63,10 +70,15 @@ You can return a function from `$effect`, which will run immediately before the
<button onclick={() => (milliseconds *= 2)}>slower</button> <button onclick={() => (milliseconds *= 2)}>slower</button>
<button onclick={() => (milliseconds /= 2)}>faster</button> <button onclick={() => (milliseconds /= 2)}>faster</button>
``` ```
<!-- codeblock:end -->
Teardown functions also run when the effect is destroyed, which happens when its parent is destroyed (for example, a component is unmounted) or the parent effect re-runs.
### Understanding dependencies ### Understanding dependencies
`$effect` automatically picks up any reactive values (`$state`, `$derived`, `$props`) that are _synchronously_ read inside its function body and registers them as dependencies. When those dependencies change, the `$effect` schedules a rerun. `$effect` automatically picks up any reactive values (`$state`, `$derived`, `$props`) that are _synchronously_ read inside its function body (including indirectly, via function calls) and registers them as dependencies. When those dependencies change, the `$effect` schedules a re-run.
If `$state` and `$derived` are used directly inside the `$effect` (for example, during creation of a [reactive class](https://svelte.dev/docs/svelte/$state#Classes)), those values will _not_ be treated as dependencies.
Values that are read _asynchronously_ — after an `await` or inside a `setTimeout`, for example — will not be tracked. Here, the canvas will be repainted when `color` changes, but not when `size` changes ([demo](/playground/untitled#H4sIAAAAAAAAE31T246bMBD9lZF3pWSlBEirfaEQqdo_2PatVIpjBrDkGGQPJGnEv1e2IZfVal-wfHzmzJyZ4cIqqdCy9M-F0blDlnqArZjmB3f72XWRHVCRw_bc4me4aDWhJstSlllhZEfbQhekkMDKfwg5PFvihMvX5OXH_CJa1Zrb0-Kpqr5jkiwC48rieuDWQbqgZ6wqFLRcvkC-hYvnkWi1dWqa8ESQTxFRjfQWsOXiWzmr0sSLhEJu3p1YsoJkNUcdZUnN9dagrBu6FVRQHAM10sJRKgUG16bXcGxQ44AGdt7SDkTDdY02iqLHnJVU6hedlWuIp94JW6Tf8oBt_8GdTxlF0b4n0C35ZLBzXb3mmYn3ae6cOW74zj0YVzDNYXRHFt9mprNgHfZSl6mzml8CMoLvTV6wTZIUDEJv5us2iwMtiJRyAKG4tXnhl8O0yhbML0Wm-B7VNlSSSd31BG7z8oIZZ6dgIffAVY_5xdU9Qrz1Bnx8fCfwtZ7v8Qc9j3nB8PqgmMWlHIID6-bkVaPZwDySfWtKNGtquxQ23Qlsq2QJT0KIqb8dL0up6xQ2eIBkAg_c1FI_YqW0neLnFCqFpwmreedJYT7XX8FVOBfwWRhXstZrSXiwKQjUhOZeMIleb5JZfHWn2Yq5pWEpmR7Hv-N_wEqT8hEEAAA=)): Values that are read _asynchronously_ — after an `await` or inside a `setTimeout`, for example — will not be tracked. Here, the canvas will be repainted when `color` changes, but not when `size` changes ([demo](/playground/untitled#H4sIAAAAAAAAE31T246bMBD9lZF3pWSlBEirfaEQqdo_2PatVIpjBrDkGGQPJGnEv1e2IZfVal-wfHzmzJyZ4cIqqdCy9M-F0blDlnqArZjmB3f72XWRHVCRw_bc4me4aDWhJstSlllhZEfbQhekkMDKfwg5PFvihMvX5OXH_CJa1Zrb0-Kpqr5jkiwC48rieuDWQbqgZ6wqFLRcvkC-hYvnkWi1dWqa8ESQTxFRjfQWsOXiWzmr0sSLhEJu3p1YsoJkNUcdZUnN9dagrBu6FVRQHAM10sJRKgUG16bXcGxQ44AGdt7SDkTDdY02iqLHnJVU6hedlWuIp94JW6Tf8oBt_8GdTxlF0b4n0C35ZLBzXb3mmYn3ae6cOW74zj0YVzDNYXRHFt9mprNgHfZSl6mzml8CMoLvTV6wTZIUDEJv5us2iwMtiJRyAKG4tXnhl8O0yhbML0Wm-B7VNlSSSd31BG7z8oIZZ6dgIffAVY_5xdU9Qrz1Bnx8fCfwtZ7v8Qc9j3nB8PqgmMWlHIID6-bkVaPZwDySfWtKNGtquxQ23Qlsq2QJT0KIqb8dL0up6xQ2eIBkAg_c1FI_YqW0neLnFCqFpwmreedJYT7XX8FVOBfwWRhXstZrSXiwKQjUhOZeMIleb5JZfHWn2Yq5pWEpmR7Hv-N_wEqT8hEEAAA=)):
@ -125,17 +137,35 @@ An effect only reruns when the object it reads changes, not when a property insi
<p>{state.value} doubled is {derived.value}</p> <p>{state.value} doubled is {derived.value}</p>
``` ```
An effect only depends on the values that it read the last time it ran. If `a` is true, changes to `b` will [not cause this effect to rerun](/playground/untitled#H4sIAAAAAAAAE3WQ0WrDMAxFf0U1hTow1vcsMfQ7lj3YjlxEXTvEymC4_vfFC6Ewtidxde8RkrJw5DGJ9j2LoO8oWnGZJvEi-GuqIn2iZ1x1istsa6dLdqaJ1RAG9sigoYdjYs0onfYJm7fdMX85q3dE59CylA30CnJtDWxjSNHjq49XeZqXEChcT9usLUAOpIbHA0yzM78oColGhDVofLS3neZSS6mqOz-XD51ZmGOAGKwne-vztk-956CL0kAJsi7decupf4l658EUZX4I8yTWt93jSI5wFC3PC5aP8g0Aje5DcQEAAA==): An effect only depends on the values that it read the last time it ran. This has interesting implications for effects that have conditional code.
For instance, if `condition` is `true` in the code snippet below, the code inside the `if` block will run and `color` will be evaluated. This means that changes to either `condition` or `color` [will cause the effect to re-run](/playground/untitled#H4sIAAAAAAAAE21RQW6DMBD8ytaNBJHaJFLViwNIVZ8RcnBgXVk1xsILTYT4e20TQg89IOPZ2fHM7siMaJBx9tmaWpFqjQNlAKXEihx7YVJpdIyfRkY3G4gB8Pi97cPanRtQU8AuwuF_eNUaQuPlOMtc1SlLRWlKUo1tOwJflUikQHZtA0klzCDc64Imx0ANn8bInV1CDhtHgjClrsftcSXotluLybOUb3g4JJHhOZs5WZpuIS9gjNqkJKQP5e2ClrR4SMdZ13E4xZ8zTPOTJU2A2uE_PQ9COCI926_hTVarIU4hu_REPlBrKq2q73ycrf1N-vS4TMUsulaVg3EtR8H9rFgsg8uUsT1B2F9eshigZHBRpuaD0D3mY8Qm2BfB5N2YyRzdNEYVDy0Ja-WsFjcOUuP1HvFLWA6H3XuHTUSmmDV2--0TXonxsKbp7G9C6R__NONS-MFNvxj_d6mBAgAA).
Conversely, if `condition` is `false`, `color` will not be evaluated, and the effect will _only_ re-run again when `condition` changes.
```ts ```ts
let a = false; // @filename: ambient.d.ts
let b = false; declare module 'canvas-confetti' {
interface ConfettiOptions {
colors: string[];
}
function confetti(opts?: ConfettiOptions): void;
export default confetti;
}
// @filename: index.js
// ---cut--- // ---cut---
$effect(() => { import confetti from 'canvas-confetti';
console.log('running');
let condition = $state(true);
let color = $state('#ff3e00');
if (a || b) { $effect(() => {
console.log('inside if block'); if (condition) {
confetti({ colors: [color] });
} else {
confetti();
} }
}); });
``` ```
@ -179,9 +209,11 @@ Apart from the timing, `$effect.pre` works exactly like `$effect`.
## `$effect.tracking` ## `$effect.tracking`
The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template ([demo](/playground/untitled#H4sIAAAAAAAACn3PwYrCMBDG8VeZDYIt2PYeY8Dn2HrIhqkU08nQjItS-u6buAt7UDzmz8ePyaKGMWBS-nNRcmdU-hHUTpGbyuvI3KZvDFLal0v4qvtIgiSZUSb5eWSxPfWSc4oB2xDP1XYk8HHiSHkICeXKeruDDQ4Demlldv4y0rmq6z10HQwuJMxGVv4mVVXDwcJS0jP9u3knynwtoKz1vifT_Z9Jhm0WBCcOTlDD8kyspmML5qNpHg40jc3fFryJ0iWsp_UHgz3180oBAAA=)): The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template:
<!-- codeblock:start {"title":"$effect.tracking()"} -->
```svelte ```svelte
<!--- file: App.svelte --->
<script> <script>
console.log('in component setup:', $effect.tracking()); // false console.log('in component setup:', $effect.tracking()); // false
@ -192,73 +224,55 @@ The `$effect.tracking` rune is an advanced feature that tells you whether or not
<p>in template: {$effect.tracking()}</p> <!-- true --> <p>in template: {$effect.tracking()}</p> <!-- true -->
``` ```
<!-- codeblock:end -->
This allows you to (for example) add things like subscriptions without causing memory leaks, by putting them in child effects. Here's a `readable` function that listens to changes from a callback function as long as it's inside a tracking context: It is used to implement abstractions like [`createSubscriber`](/docs/svelte/svelte-reactivity#createSubscriber), which will create listeners to update reactive values but _only_ if those values are being tracked (rather than, for example, read inside an event handler).
```ts ## `$effect.pending`
import { tick } from 'svelte';
export default function readable<T>( When using [`await`](await-expressions) in components, the `$effect.pending()` rune tells you how many promises are pending in the current [boundary](svelte-boundary), not including child boundaries:
initial_value: T,
start: (callback: (update: (v: T) => T) => T) => () => void
) {
let value = $state(initial_value);
let subscribers = 0; <!-- codeblock:start {"title":"$effect.pending"} -->
let stop: null | (() => void) = null; ```svelte
<!--- file: App.svelte --->
<script>
let a = $state(1);
let b = $state(2);
return { async function add(a, b) {
get value() { await new Promise((f) => setTimeout(f, 500)); // artificial delay
// If in a tracking context ... return a + b;
if ($effect.tracking()) {
$effect(() => {
// ...and there's no subscribers yet...
if (subscribers === 0) {
// ...invoke the function and listen to changes to update state
stop = start((fn) => (value = fn(value)));
} }
</script>
subscribers++; <button onclick={() => a++}>a++</button>
<button onclick={() => b++}>b++</button>
// The return callback is called once a listener unlistens <p>{a} + {b} = {await add(a, b)}</p>
return () => {
tick().then(() => {
subscribers--;
// If it was the last subscriber...
if (subscribers === 0) {
// ...stop listening to changes
stop?.();
stop = null;
}
});
};
});
}
return value; {#if $effect.pending()}
} <p>pending promises: {$effect.pending()}</p>
}; {/if}
}
``` ```
<!-- codeblock:end -->
## `$effect.root` ## `$effect.root`
The `$effect.root` rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for nested effects that you want to manually control. This rune also allows for the creation of effects outside of the component initialisation phase. The `$effect.root` rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for nested effects that you want to manually control. This rune also allows for the creation of effects outside of the component initialisation phase.
```svelte ```js
<script> const destroy = $effect.root(() => {
let count = $state(0);
const cleanup = $effect.root(() => {
$effect(() => { $effect(() => {
console.log(count); // setup
}); });
return () => { return () => {
console.log('effect root cleanup'); // cleanup
}; };
}); });
</script>
// later...
destroy();
``` ```
## When not to use `$effect` ## When not to use `$effect`
@ -288,11 +302,15 @@ In general, `$effect` is best considered something of an escape hatch — useful
> [!NOTE] For things that are more complicated than a simple expression like `count * 2`, you can also use `$derived.by`. > [!NOTE] For things that are more complicated than a simple expression like `count * 2`, you can also use `$derived.by`.
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/playground/untitled#H4sIAAAAAAAACpVRy26DMBD8FcvKgUhtoIdeHBwp31F6MGSJkBbHwksEQvx77aWQqooq9bgzOzP7mGTdIHipPiZJowOpGJAv0po2VmfnDv4OSBErjYdneHWzBJaCjcx91TWOToUtCIEE3cig0OIty44r5l1oDtjOkyFIsv3GINQ_CNYyGegd1DVUlCR7oU9iilDUcP8S8roYs9n8p2wdYNVFm4csTx872BxNCcjr5I11fdgonEkXsjP2CoUUZWMv6m6wBz2x7yxaM-iJvWeRsvSbSVeUy5i0uf8vKA78NIeJLSZWv1I8jQjLdyK4XuTSeIdmVKJGGI4LdjVOiezwDu1yG74My8PLCQaSiroe5s_5C2PHrkVGAgAA)): If you're using an effect because you want to be able to reassign the derived value (to build an optimistic UI, for example) note that [deriveds can be directly overridden]($derived#Overriding-derived-values) as of Svelte 5.25.
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Instead of using effects for this...
<!-- codeblock:start {"title":"Setting state in effects (don't do this!)"} -->
```svelte ```svelte
<!--- file: App.svelte --->
<script> <script>
let total = 100; const total = 100;
let spent = $state(0); let spent = $state(0);
let left = $state(total); let left = $state(total);
@ -314,64 +332,48 @@ You might be tempted to do something convoluted with effects to link one value t
<input type="range" bind:value={left} max={total} /> <input type="range" bind:value={left} max={total} />
{left}/{total} left {left}/{total} left
</label> </label>
<style>
label {
display: flex;
gap: 0.5em;
}
</style>
``` ```
<!-- codeblock:end -->
Instead, use callbacks where possible ([demo](/playground/untitled#H4sIAAAAAAAACo1SMW6EMBD8imWluFMSIEUaDiKlvy5lSOHjlhOSMRZeTiDkv8deMEEJRcqdmZ1ZjzzxqpZgePo5cRw18JQA_sSVaPz0rnVk7iDRYxdhYA8vW4Wg0NnwzJRdrfGtUAVKQIYtCsly9pIkp4AZ7cQOezAoEA7JcWUkVBuCdol0dNWrEutWsV5fHfnhPQ5wZJMnCwyejxCh6G6A0V3IHk4zu_jOxzzPBxBld83PTr7xXrb3rUNw8PbiYJ3FP22oTIoLSComq5XuXTeu8LzgnVA3KDgj13wiQ8taRaJ82rzXskYM-URRlsXktejjgNLoo9e4fyf70_8EnwncySX1GuunX6kGRwnzR_BgaPNaGy3FmLJKwrCUeBM6ZUn0Cs2mOlp3vwthQJ5i14P9st9vZqQlsQIAAA==)): ...use `oninput` callbacks or — better still — [function bindings](bind#Function-bindings) where possible:
<!-- codeblock:start {"title":"Setting state with function bindings"} -->
```svelte ```svelte
<!--- file: App.svelte --->
<script> <script>
let total = 100; const total = 100;
let spent = $state(0); let spent = $state(0);
let left = $state(total); let left = $derived(total - spent);
function updateSpent(e) {
spent = +e.target.value;
left = total - spent;
}
function updateLeft(e) { +++ function updateLeft(left) {
left = +e.target.value;
spent = total - left; spent = total - left;
} }+++
</script> </script>
<label> <label>
<input type="range" value={spent} oninput={updateSpent} max={total} /> <input type="range" bind:value={spent} max={total} />
{spent}/{total} spent {spent}/{total} spent
</label> </label>
<label> <label>
<input type="range" value={left} oninput={updateLeft} max={total} /> <input type="range" +++bind:value={() => left, updateLeft}+++ max={total} />
{left}/{total} left {left}/{total} left
</label> </label>
```
If you need to use bindings, for whatever reason (for example when you want some kind of "writable `$derived`"), consider using getters and setters to synchronise state ([demo](/playground/untitled#H4sIAAAAAAAACpWRwW6DMBBEf8WyekikFOihFwcq9TvqHkyyQUjGsfCCQMj_XnvBNKpy6Qn2DTOD1wu_tRocF18Lx9kCFwT4iRvVxenT2syNoDGyWjl4xi93g2AwxPDSXfrW4oc0EjUgwzsqzSr2VhTnxJwNHwf24lAhHIpjVDZNwy1KS5wlNoGMSg9wOCYksQccerMlv65p51X0p_Xpdt_4YEy9yTkmV3z4MJT579-bUqsaNB2kbI0dwlnCgirJe2UakJzVrbkKaqkWivasU1O1ULxnOVk3JU-Uxti0p_-vKO4no_enbQ_yXhnZn0aHs4b1jiJMK7q2zmo1C3bTMG3LaZQVrMjeoSPgaUtkDxePMCEX2Ie6b_8D4WyJJEwCAAA=)):
```svelte
<script>
let total = 100;
let spent = $state(0);
let left = { <style>
get value() { label {
return total - spent; display: flex;
}, gap: 0.5em;
set value(v) {
spent = total - v;
} }
}; </style>
</script>
<label>
<input type="range" bind:value={spent} max={total} />
{spent}/{total} spent
</label>
<label>
<input type="range" bind:value={left.value} max={total} />
{left.value}/{total} left
</label>
``` ```
<!-- codeblock:end -->
If you absolutely have to update `$state` within an effect and run into an infinite loop because you read and write to the same `$state`, use [untrack](svelte#untrack). If you absolutely have to update `$state` within an effect and run into an infinite loop because you read and write to the same `$state`, use [untrack](svelte#untrack).

@ -1,5 +1,6 @@
--- ---
title: $props title: $props
tags: rune-props
--- ---
The inputs to a component are referred to as _props_, which is short for _properties_. You pass props to components just like you pass attributes to elements: The inputs to a component are referred to as _props_, which is short for _properties_. You pass props to components just like you pass attributes to elements:
@ -37,7 +38,7 @@ On the other side, inside `MyComponent.svelte`, we can receive props with the `$
## Fallback values ## Fallback values
Destructuring allows us to declare fallback values, which are used if the parent component does not set a given prop: Destructuring allows us to declare fallback values, which are used if the parent component does not set a given prop (or the value is `undefined`):
```js ```js
let { adjective = 'happy' } = $props(); let { adjective = 'happy' } = $props();
@ -63,8 +64,9 @@ let { a, b, c, ...others } = $props();
## Updating props ## Updating props
References to a prop inside a component update when the prop itself updates — when `count` changes in `App.svelte`, it will also change inside `Child.svelte`. But the child component is able to temporarily override the prop value, which can be useful for unsaved ephemeral state ([demo](/playground/untitled#H4sIAAAAAAAAE6WQ0WrDMAxFf0WIQR0Wmu3VTQJln7HsIfVcZubIxlbGRvC_DzuBraN92qPula50tODZWB1RPi_IX16jLALWSOOUq6P3-_ihLWftNEZ9TVeOWBNHlNhGFYznfqCBzeRdYHh6M_YVzsFNsNs3pdpGd4eBcqPVDMrNxNDBXeSRtXioDgO1zU8ataeZ2RE4Utao924RFXQ9iHXwvoPHKpW1xY4g_Bg0cSVhKS0p560Za95612ZC02ONrD8ZJYdZp_rGQ37ff_mSP86Np2TWZaNNmdcH56P4P67K66_SXoK9pG-5dF5Z9QEAAA==)): References to a prop inside a component update when the prop itself updates — when `count` changes in `App.svelte`, it will also change inside `Child.svelte`. But the child component is able to temporarily override the prop value, which can be useful for unsaved ephemeral state:
<!-- codeblock:start {"title":"Temporarily updating props","selected":"Child.svelte"} -->
```svelte ```svelte
<!--- file: App.svelte ---> <!--- file: App.svelte --->
<script> <script>
@ -90,11 +92,13 @@ References to a prop inside a component update when the prop itself updates —
clicks (child): {count} clicks (child): {count}
</button> </button>
``` ```
<!-- codeblock:end -->
While you can temporarily _reassign_ props, you should not _mutate_ props unless they are [bindable]($bindable). While you can temporarily _reassign_ props, you should not _mutate_ props unless they are [bindable]($bindable).
If the prop is a regular object, the mutation will have no effect ([demo](/playground/untitled#H4sIAAAAAAAAE3WQwU7DMBBEf2W1QmorQgJXk0RC3PkBwiExG9WQrC17U4Es_ztKUkQp9OjxzM7bjcjtSKjwyfKNp1aLORA4b13ADHszUED1HFE-3eyaBcy-Mw_O5eFAg8xa1wb6T9eWhVgCKiyD9sZJ3XAjZnTWCzzuzfAKvbcjbPJieR2jm_uGy-InweXqtd0baaliBG0nFgW3kBIUNWYo9CGoxE-UsgvIpw2_oc9-LmAPJBCPDJCggqvlVtvdH9puErEMlvVg9HsVtzuoaojzkKKAfRuALVDfk5ZZW0fmy05wXcFdwyktlUs-KIinljTXrRVnm7-kL9dYLVbUAQAA)): If the prop is a regular object, the mutation will have no effect:
<!-- codeblock:start {"title":"Non-reactive props","selected":"Child.svelte"} -->
```svelte ```svelte
<!--- file: App.svelte ---> <!--- file: App.svelte --->
<script> <script>
@ -117,9 +121,11 @@ If the prop is a regular object, the mutation will have no effect ([demo](/playg
clicks: {object.count} clicks: {object.count}
</button> </button>
``` ```
<!-- codeblock:end -->
If the prop is a reactive state proxy, however, then mutations _will_ have an effect but you will see an [`ownership_invalid_mutation`](runtime-warnings#Client-warnings-ownership_invalid_mutation) warning, because the component is mutating state that does not 'belong' to it ([demo](/playground/untitled#H4sIAAAAAAAAE3WR0U7DMAxFf8VESBuiauG1WycheOEbKA9p67FA6kSNszJV-XeUZhMw2GN8r-1znUmQ7FGU4pn2UqsOes-SlSGRia3S6ET5Mgk-2OiJBZGdOh6szd0eNcdaIx3-V28NMRI7UYq1awdleVNTzaq3ZmB43CndwXYwPSzyYn4dWxermqJRI4Np3rFlqODasWRcTtAaT1zCHYSbVU3r4nsyrdPMKTUFKDYiE4yfLEoePIbsQpqfy3_nOVMuJIqg0wk1RFg7GOuWfwEbz2wIDLVatR_VtLyBagNTHFIUMCqtoZXeIfAOU1JoUJsR2IC3nWTMjt7GM4yKdyBhlAMpesvhydCC0y_i0ZagHByMh26WzUhXUUxKnpbcVnBfUwhznJnNlac7JkuIURL-2VVfwxflyrWcSQIAAA==)): If the prop is a reactive state proxy, however, then mutations _will_ have an effect but you will see an [`ownership_invalid_mutation`](runtime-warnings#Client-warnings-ownership_invalid_mutation) warning, because the component is mutating state that does not 'belong' to it:
<!-- codeblock:start {"title":"Invalid mutation","selected":"Child.svelte"} -->
```svelte ```svelte
<!--- file: App.svelte ---> <!--- file: App.svelte --->
<script> <script>
@ -146,8 +152,19 @@ If the prop is a reactive state proxy, however, then mutations _will_ have an ef
clicks: {object.count} clicks: {object.count}
</button> </button>
``` ```
<!-- codeblock:end -->
The fallback value of a prop not declared with `$bindable` is left untouched — it is not turned into a reactive state proxy — meaning mutations will not cause updates ([demo](/playground/untitled#H4sIAAAAAAAAE3WQwU7DMBBEf2VkIbUVoYFraCIh7vwA4eC4G9Wta1vxpgJZ_nfkBEQp9OjxzOzTRGHlkUQlXpy9G0gq1idCL43ppDrAD84HUYheGwqieo2CP3y2Z0EU3-En79fhRIaz1slA_-nKWSbLQVRiE9SgPTetbVkfvRsYzztttugHd8RiXU6vr-jisbWb8idhN7O3bEQhmN5ZVDyMlIorcOddv_Eufq4AGmJEuG5PilEjQrnRcoV7JCTUuJlGWq7-YHYjs7NwVhmtDnVcrlA3iLmzLLGTAdaB-j736h68Oxv-JM1I0AFjoG1OzPfX023c1nhobUoT39QeKsRzS8owM8DFTG_pE6dcVl70AQAA)) The fallback value of a prop not declared with `$bindable` is left untouched — it is not turned into a reactive state proxy — meaning mutations will not cause updates:
<!-- codeblock:start {"title":"Non-reactive fallback props","selected":"Child.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Child from './Child.svelte';
</script>
<Child />
```
```svelte ```svelte
<!--- file: Child.svelte ---> <!--- file: Child.svelte --->
@ -162,6 +179,7 @@ The fallback value of a prop not declared with `$bindable` is left untouched —
clicks: {object.count} clicks: {object.count}
</button> </button>
``` ```
<!-- codeblock:end -->
In summary: don't mutate props. Either use callback props to communicate changes, or — if parent and child should share the same object — use the [`$bindable`]($bindable) rune. In summary: don't mutate props. Either use callback props to communicate changes, or — if parent and child should share the same object — use the [`$bindable`]($bindable) rune.
@ -196,4 +214,29 @@ You can, of course, separate the type declaration from the annotation:
</script> </script>
``` ```
> [!NOTE] Interfaces for native DOM elements are provided in the `svelte/elements` module (see [Typing wrapper components](typescript#Typing-wrapper-components))
If your component exposes [snippet](snippet) props like `children`, these should be typed using the `Snippet` interface imported from `'svelte'` — see [Typing snippets](snippet#Typing-snippets) for examples.
Adding types is recommended, as it ensures that people using your component can easily discover which props they should provide. Adding types is recommended, as it ensures that people using your component can easily discover which props they should provide.
## `$props.id()`
This rune, added in version 5.20.0, generates an ID that is unique to the current component instance. When hydrating a server-rendered component, the value will be consistent between server and client.
This is useful for linking elements via attributes like `for` and `aria-labelledby`.
```svelte
<script>
const uid = $props.id();
</script>
<form>
<label for="{uid}-firstname">First Name: </label>
<input id="{uid}-firstname" type="text" />
<label for="{uid}-lastname">Last Name: </label>
<input id="{uid}-lastname" type="text" />
</form>
```

@ -4,7 +4,7 @@ title: $bindable
Ordinarily, props go one way, from parent to child. This makes it easy to understand how data flows around your app. Ordinarily, props go one way, from parent to child. This makes it easy to understand how data flows around your app.
In Svelte, component props can be _bound_, which means that data can also flow _up_ from child to parent. This isn't something you should do often, but it can simplify your code if used sparingly and carefully. In Svelte, component props can be _bound_, which means that data can also flow _up_ from child to parent. This isn't something you should do often — overuse can make your data flow unpredictable and your components harder to maintain — but it can simplify your code if used sparingly and carefully.
It also means that a state proxy can be _mutated_ in the child. It also means that a state proxy can be _mutated_ in the child.
@ -33,7 +33,7 @@ Now, a component that uses `<FancyInput>` can add the [`bind:`](bind) directive
<!-- prettier-ignore --> <!-- prettier-ignore -->
```svelte ```svelte
/// App.svelte /// file: App.svelte
<script> <script>
import FancyInput from './FancyInput.svelte'; import FancyInput from './FancyInput.svelte';

@ -1,12 +1,15 @@
--- ---
title: $inspect title: $inspect
tags: rune-inspect
--- ---
> [!NOTE] `$inspect` only works during development. In a production build it becomes a noop. > [!NOTE] `$inspect` only works during development. In a production build it becomes a noop.
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire ([demo](/playground/untitled#H4sIAAAAAAAACkWQ0YqDQAxFfyUMhSotdZ-tCvu431AXtGOqQ2NmmMm0LOK_r7Utfby5JzeXTOpiCIPKT5PidkSVq2_n1F7Jn3uIcEMSXHSw0evHpAjaGydVzbUQCmgbWaCETZBWMPlKj29nxBDaHj_edkAiu12JhdkYDg61JGvE_s2nR8gyuBuiJZuDJTyQ7eE-IEOzog1YD80Lb0APLfdYc5F9qnFxjiKWwbImo6_llKRQVs-2u91c_bD2OCJLkT3JZasw7KLA2XCX31qKWE6vIzNk1fKE0XbmYrBTufiI8-_8D2cUWBA_AQAA)): The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire:
<!-- codeblock:start {"title":"$inspect(...)"} -->
```svelte ```svelte
<!--- file: App.svelte --->
<script> <script>
let count = $state(0); let count = $state(0);
let message = $state('hello'); let message = $state('hello');
@ -17,12 +20,17 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
<button onclick={() => count++}>Increment</button> <button onclick={() => count++}>Increment</button>
<input bind:value={message} /> <input bind:value={message} />
``` ```
<!-- codeblock:end -->
On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations).
## $inspect(...).with ## $inspect(...).with
`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` ([demo](/playground/untitled#H4sIAAAAAAAACkVQ24qDMBD9lSEUqlTqPlsj7ON-w7pQG8c2VCchmVSK-O-bKMs-DefKYRYx6BG9qL4XQd2EohKf1opC8Nsm4F84MkbsTXAqMbVXTltuWmp5RAZlAjFIOHjuGLOP_BKVqB00eYuKs82Qn2fNjyxLtcWeyUE2sCRry3qATQIpJRyD7WPVMf9TW-7xFu53dBcoSzAOrsqQNyOe2XUKr0Xi5kcMvdDB2wSYO-I9vKazplV1-T-d6ltgNgSG1KjVUy7ZtmdbdjqtzRcphxMS1-XubOITJtPrQWMvKnYB15_1F7KKadA_AQAA)): `$inspect(...)` returns an object with a `with` method, which you can invoke with a callback that will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect`:
<!-- codeblock:start {"title":"$inspect(...).with(...)"} -->
```svelte ```svelte
<!--- file: App.svelte --->
<script> <script>
let count = $state(0); let count = $state(0);
@ -35,13 +43,7 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
<button onclick={() => count++}>Increment</button> <button onclick={() => count++}>Increment</button>
``` ```
<!-- codeblock:end -->
A convenient way to find the origin of some change is to pass `console.trace` to `with`:
```js
// @errors: 2304
$inspect(stuff).with(console.trace);
```
## $inspect.trace(...) ## $inspect.trace(...)
@ -52,6 +54,7 @@ This rune, added in 5.14, causes the surrounding function to be _traced_ in deve
import { doSomeWork } from './elsewhere'; import { doSomeWork } from './elsewhere';
$effect(() => { $effect(() => {
+++// $inspect.trace must be the first statement of a function body+++
+++$inspect.trace();+++ +++$inspect.trace();+++
doSomeWork(); doSomeWork();
}); });

@ -2,7 +2,7 @@
title: $host title: $host
--- ---
When compiling a component as a custom element, the `$host` rune provides access to the host element, allowing you to (for example) dispatch custom events ([demo](/playground/untitled#H4sIAAAAAAAAE41Ry2rDMBD8FSECtqkTt1fHFpSSL-ix7sFRNkTEXglrnTYY_3uRlDgxTaEHIfYxs7szA9-rBizPPwZOZwM89wmecqxbF70as7InaMjltrWFR3mpkQDJ8pwXVnbKkKiwItUa3RGLVtk7gTHQXRDR2lXda4CY1D0SK9nCUk0QPyfrCovsRoNFe17aQOAwGncgO2gBqRzihJXiQrEs2csYOhQ-7HgKHaLIbpRhhBG-I2eD_8ciM4KnnOCbeE5dD2P6h0Dz0-Yi_arNhPLJXBtSGi2TvSXdbpqwdsXvjuYsC1veabvvUTog2ylrapKH2G2XsMFLS4uDthQnq2t1cwKkGOGLvYU5PvaQxLsxOkPmsm97Io1Mo2yUPF6VnOZFkw1RMoopKLKAE_9gmGxyDFMwMcwN-Bx_ABXQWmOtAgAA)): When compiling a component as a [custom element](custom-elements), the `$host` rune provides access to the host element, allowing you to (for example) dispatch custom events ([demo](/playground/untitled#H4sIAAAAAAAAE41Ry2rDMBD8FSECtqkTt1fHFpSSL-ix7sFRNkTEXglrnTYY_3uRlDgxTaEHIfYxs7szA9-rBizPPwZOZwM89wmecqxbF70as7InaMjltrWFR3mpkQDJ8pwXVnbKkKiwItUa3RGLVtk7gTHQXRDR2lXda4CY1D0SK9nCUk0QPyfrCovsRoNFe17aQOAwGncgO2gBqRzihJXiQrEs2csYOhQ-7HgKHaLIbpRhhBG-I2eD_8ciM4KnnOCbeE5dD2P6h0Dz0-Yi_arNhPLJXBtSGi2TvSXdbpqwdsXvjuYsC1veabvvUTog2ylrapKH2G2XsMFLS4uDthQnq2t1cwKkGOGLvYU5PvaQxLsxOkPmsm97Io1Mo2yUPF6VnOZFkw1RMoopKLKAE_9gmGxyDFMwMcwN-Bx_ABXQWmOtAgAA)):
<!-- prettier-ignore --> <!-- prettier-ignore -->
```svelte ```svelte

@ -82,12 +82,14 @@ As with elements, `name={name}` can be replaced with the `{name}` shorthand.
<Widget foo={bar} answer={42} text="hello" /> <Widget foo={bar} answer={42} text="hello" />
``` ```
## Spread attributes
_Spread attributes_ allow many attributes or properties to be passed to an element or component at once. _Spread attributes_ allow many attributes or properties to be passed to an element or component at once.
An element or component can have multiple spread attributes, interspersed with regular ones. An element or component can have multiple spread attributes, interspersed with regular ones. Order matters — if `things.a` exists it will take precedence over `a="b"`, while `c="d"` would take precedence over `things.c`:
```svelte ```svelte
<Widget {...things} /> <Widget a="b" {...things} c="d" />
``` ```
## Events ## Events
@ -154,6 +156,8 @@ A JavaScript expression can be included as text by surrounding it with curly bra
{expression} {expression}
``` ```
Expressions that are `null` or `undefined` will be omitted; all others are [coerced to strings](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#string_coercion).
Curly braces can be included in a Svelte template by using their [HTML entity](https://developer.mozilla.org/docs/Glossary/Entity) strings: `&lbrace;`, `&lcub;`, or `&#123;` for `{` and `&rbrace;`, `&rcub;`, or `&#125;` for `}`. Curly braces can be included in a Svelte template by using their [HTML entity](https://developer.mozilla.org/docs/Glossary/Entity) strings: `&lbrace;`, `&lcub;`, or `&#123;` for `{` and `&rbrace;`, `&rcub;`, or `&#125;` for `}`.
If you're using a regular expression (`RegExp`) [literal notation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#literal_notation_and_constructor), you'll need to wrap it in parentheses. If you're using a regular expression (`RegExp`) [literal notation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#literal_notation_and_constructor), you'll need to wrap it in parentheses.
@ -185,7 +189,7 @@ You can use HTML comments inside components.
Comments beginning with `svelte-ignore` disable warnings for the next block of markup. Usually, these are accessibility warnings; make sure that you're disabling them for a good reason. Comments beginning with `svelte-ignore` disable warnings for the next block of markup. Usually, these are accessibility warnings; make sure that you're disabling them for a good reason.
```svelte ```svelte
<!-- svelte-ignore a11y-autofocus --> <!-- svelte-ignore a11y_autofocus -->
<input bind:value={name} autofocus /> <input bind:value={name} autofocus />
``` ```

@ -1,5 +1,6 @@
--- ---
title: {#if ...} title: {#if ...}
tags: template-if
--- ---
```svelte ```svelte

@ -1,5 +1,6 @@
--- ---
title: {#each ...} title: {#each ...}
tags: template-each
--- ---
```svelte ```svelte
@ -12,7 +13,9 @@ title: {#each ...}
{#each expression as name, index}...{/each} {#each expression as name, index}...{/each}
``` ```
Iterating over values can be done with an each block. The values in question can be arrays, array-like objects (i.e. anything with a `length` property), or iterables like `Map` and `Set` — in other words, anything that can be used with `Array.from`. Iterating over values can be done with an each block. The values in question can be arrays, array-like objects (i.e. anything with a `length` property), or iterables like `Map` and `Set`. (Internally, they are converted to arrays with [`Array.from`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from).)
If the value is `null` or `undefined`, it is treated the same as an empty array (which will cause [else blocks](#Else-blocks) to be rendered, where applicable).
```svelte ```svelte
<h1>Shopping list</h1> <h1>Shopping list</h1>
@ -43,7 +46,9 @@ An each block can also specify an _index_, equivalent to the second argument in
{#each expression as name, index (key)}...{/each} {#each expression as name, index (key)}...{/each}
``` ```
If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to diff the list when data changes, rather than adding or removing items at the end. The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change. If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to intelligently update the list when data changes by inserting, moving and deleting items, rather than adding or removing items at the end and updating the state in the middle.
The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change.
```svelte ```svelte
{#each items as item (item.id)} {#each items as item (item.id)}
@ -84,9 +89,11 @@ You can freely use destructuring and rest patterns in each blocks.
{#each expression, index}...{/each} {#each expression, index}...{/each}
``` ```
In case you just want to render something `n` times, you can omit the `as` part ([demo](/playground/untitled#H4sIAAAAAAAAE3WR0W7CMAxFf8XKNAk0WsSeUEaRpn3Guoc0MbQiJFHiMlDVf18SOrZJ48259_jaVgZmxBEZZ28thgCNFV6xBdt1GgPj7wOji0t2EqI-wa_OleGEmpLWiID_6dIaQkMxhm1UdwKpRQhVzWSaVORJNdvWpqbhAYVsYQCNZk8thzWMC_DCHMZk3wPSThNQ088I3mghD9UwSwHwlLE5PMIzVFUFq3G7WUZ2OyUvU3JOuZU332wCXTRmtPy1NgzXZtUFp8WFw9536uWqpbIgPEaDsJBW90cTOHh0KGi2XsBq5-cT6-3nPauxXqHnsHJnCFZ3CvJVkyuCQ0mFF9TZyCQ162WGvteLKfG197Y3iv_pz_fmS68Hxt8iPBPj5HscP8YvCNX7uhYCAAA=)): In case you just want to render something `n` times, you can omit the `as` part:
<!-- codeblock:start {"title":"Chess board"} -->
```svelte ```svelte
<!--- file: App.svelte --->
<div class="chess-board"> <div class="chess-board">
{#each { length: 8 }, rank} {#each { length: 8 }, rank}
{#each { length: 8 }, file} {#each { length: 8 }, file}
@ -94,7 +101,22 @@ In case you just want to render something `n` times, you can omit the `as` part
{/each} {/each}
{/each} {/each}
</div> </div>
<style>
.chess-board {
display: grid;
grid-template-columns: repeat(8, 1fr);
rows: repeat(8, 1fr);
border: 1px solid black;
aspect-ratio: 1;
.black {
background: black;
}
}
</style>
``` ```
<!-- codeblock:end -->
## Else blocks ## Else blocks

@ -1,5 +1,6 @@
--- ---
title: {#key ...} title: {#key ...}
tags: template-key
--- ---
```svelte ```svelte

@ -1,5 +1,6 @@
--- ---
title: {#await ...} title: {#await ...}
tags: template-await
--- ---
```svelte ```svelte

@ -57,9 +57,11 @@ Like function declarations, snippets can have an arbitrary number of parameters,
## Snippet scope ## Snippet scope
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks ([demo](/playground/untitled#H4sIAAAAAAAAE12P0QrCMAxFfyWrwhSEvc8p-h1OcG5RC10bmkyQ0n-3HQPBx3vCPUmCemiDrOpLULYbUdXqTKR2Sj6UA7_RCKbMbvJ9Jg33XpMcW9uKQYEAIzJ3T4QD3LSUDE-PnYA4YET4uOkGMc3W5B3xZrtvbVP9HDas2GqiZHqhMW6Tr9jGbG_oOCMImcUCwrIpFk1FqRyqpRpn0cmjHdAvnrIzuscyq_4nd3dPPD01ukE_NA6qFj9hvMYvGjJADw8BAAA=))... Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks...
<!-- codeblock:start {"title":"Snippets"} -->
```svelte ```svelte
<!--- file: App.svelte --->
<script> <script>
let { message = `it's great to see you!` } = $props(); let { message = `it's great to see you!` } = $props();
</script> </script>
@ -71,6 +73,7 @@ Snippets can be declared anywhere inside your component. They can reference valu
{@render hello('alice')} {@render hello('alice')}
{@render hello('bob')} {@render hello('bob')}
``` ```
<!-- codeblock:end -->
...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings): ...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings):
@ -91,9 +94,11 @@ Snippets can be declared anywhere inside your component. They can reference valu
{@render x()} {@render x()}
``` ```
Snippets can reference themselves and each other ([demo](/playground/untitled#H4sIAAAAAAAAE2WPTQqDMBCFrxLiRqH1Zysi7TlqF1YnENBJSGJLCYGeo5tesUeosfYH3c2bee_jjaWMd6BpfrAU6x5oTvdS0g01V-mFPkNnYNRaDKrxGxto5FKCIaeu1kYwFkauwsoUWtZYPh_3W5FMY4U2mb3egL9kIwY0rbhgiO-sDTgjSEqSTvIDs-jiOP7i_MHuFGAL6p9BtiSbOTl0GtzCuihqE87cqtyam6WRGz_vRcsZh5bmRg3gju4Fptq_kzQBAAA=)): Snippets can reference themselves and each other:
<!-- codeblock:start {"title":"Self-referencing snippets"} -->
```svelte ```svelte
<!--- file: App.svelte --->
{#snippet blastoff()} {#snippet blastoff()}
<span>🚀</span> <span>🚀</span>
{/snippet} {/snippet}
@ -109,12 +114,17 @@ Snippets can reference themselves and each other ([demo](/playground/untitled#H4
{@render countdown(10)} {@render countdown(10)}
``` ```
<!-- codeblock:end -->
## Passing snippets to components ## Passing snippets to components
Within the template, snippets are values just like any other. As such, they can be passed to components as props ([demo](/playground/untitled#H4sIAAAAAAAAE3VS247aMBD9lZGpBGwDASRegonaPvQL2qdlH5zYEKvBNvbQLbL875VzAcKyj3PmzJnLGU8UOwqSkd8KJdaCk4TsZS0cyV49wYuJuQiQpGd-N2bu_ooaI1YwJ57hpVYoFDqSEepKKw3mO7VDeTTaIvxiRS1gb_URxvO0ibrS8WanIrHUyiHs7Vmigy28RmyHHmKvDMbMmFq4cQInvGSwTsBYWYoMVhCSB2rBFFPsyl0uruTlR3JZCWvlTXl1Yy_mawiR_rbZKZrellJ-5JQ0RiBUgnFhJ9OGR7HKmwVoilXeIye8DOJGfYCgRlZ3iE876TBsZPX7hPdteO75PC4QaIo8vwNPePmANQ2fMeEFHrLD7rR1jTNkW986E8C3KwfwVr8HSHOSEBT_kGRozyIkn_zQveXDL3rIfPJHtUDwzShJd_Qk3gQCbOGLsdq4yfTRJopRuin3I7nv6kL7ARRjmLdBDG3uv1mhuLA3V2mKtqNEf_oCn8p9aN-WYqH5peP4kWBl1UwJzAEPT9U7K--0fRrrWnPTXpCm1_EVdXjpNmlA8G1hPPyM1fKgMqjFHjctXGjLhZ05w0qpDhksGrybuNEHtJnCalZWsuaTlfq6nPaaBSv_HKw-K57BjzOiVj9ZKQYKzQjZodYFqydYTRN4gPhVzTDO2xnma3HsVWjaLjT8nbfwHy7Q5f2dBAAA)): ### Explicit props
Within the template, snippets are values just like any other. As such, they can be passed to components as props:
<!-- codeblock:start {"title":"Explicit snippet props"} -->
```svelte ```svelte
<!--- file: App.svelte --->
<script> <script>
import Table from './Table.svelte'; import Table from './Table.svelte';
@ -139,15 +149,65 @@ Within the template, snippets are values just like any other. As such, they can
<td>{d.qty * d.price}</td> <td>{d.qty * d.price}</td>
{/snippet} {/snippet}
<Table data={fruits} {header} {row} /> <Table data={fruits} +++{header} {row}+++ />
```
```svelte
<!--- file: Table.svelte --->
<script>
let { data, header, row } = $props();
</script>
<table>
{#if header}
<thead>
<tr>{@render header()}</tr>
</thead>
{/if}
<tbody>
{#each data as d}
<tr>{@render row(d)}</tr>
{/each}
</tbody>
</table>
<style>
table {
text-align: left;
border-spacing: 0;
}
tbody tr:nth-child(2n+1) {
background: ButtonFace;
}
table :global(th), table :global(td) {
padding: 0.5em;
}
</style>
``` ```
<!-- codeblock:end -->
Think about it like passing content instead of data to a component. The concept is similar to slots in web components. Think about it like passing content instead of data to a component. The concept is similar to slots in web components.
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component ([demo](/playground/untitled#H4sIAAAAAAAAE3VSTa_aMBD8Kyu_SkAbCA-JSzBR20N_QXt6vIMTO8SqsY29tI2s_PcqTiB8vaPHs7MzuxuIZgdBMvJLo0QlOElIJZXwJHsLBBvb_XUASc7Mb9Yu_B-hsMMK5sUzvDQahUZPMkJ96aTFfKd3KA_WOISfrFACKmcOMFmk8TWUTjY73RFLoz1C5U4SPWzhrcN2GKDrlcGEWauEnyRwxCaDdQLWyVJksII2uaMWTDPNLtzX5YX8-kgua-GcHJVXI3u5WEPb0d83O03TMZSmfRzOkG1Db7mNacOL19JagVALxoWbztq-H8U6j0SaYp2P2BGbOyQ2v8PQIFMXLKRDk177pq0zf6d8bMrzwBdd0pamyPMb-IjNEzS2f86Gz_Dwf-2F9nvNSUJQ_EOSoTuJNvngqK5v4Pas7n4-OCwlEEJcQTIMO-nSQwtb-GSdsX46e9gbRoP9yGQ11I0rEuycunu6PHx1QnPhxm3SFN15MOlYEFJZtf0dUywMbwZOeBGsrKNLYB54-1R9WNqVdki7usim6VmQphf7mnpshiQRhNAXdoOfMyX3OgMlKtz0cGEcF27uLSul3mewjPjgOOoDukxjPS9rqfh0pb-8zs6aBSt_7505aZ7B9xOi0T9YKW4UooVsr0zB1BTrWQJ3EL-oWcZ572GxFoezCk37QLe3897-B2i2U62uBAAA)): ### Implicit props
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component:
<!-- codeblock:start {"title":"Implicit snippet props"} -->
```svelte ```svelte
<!-- this is semantically the same as the above --> <!--- file: App.svelte --->
<script>
import Table from './Table.svelte';
const fruits = [
{ name: 'apples', qty: 5, price: 2 },
{ name: 'bananas', qty: 10, price: 1 },
{ name: 'cherries', qty: 20, price: 0.5 }
];
</script>
<Table data={fruits}> <Table data={fruits}>
{#snippet header()} {#snippet header()}
<th>fruit</th> <th>fruit</th>
@ -165,10 +225,54 @@ As an authoring convenience, snippets declared directly _inside_ a component imp
</Table> </Table>
``` ```
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet ([demo](/playground/untitled#H4sIAAAAAAAAE3WOQQrCMBBFrzIMggql3ddY1Du4si5sOmIwnYRkFKX07lKqglqX8_7_w2uRDw1hjlsWI5ZqTPBoLEXMdy3K3fdZDzB5Ndfep_FKVnpWHSKNce1YiCVijirqYLwUJQOYxrsgsLmIOIZjcA1M02w4n-PpomSVvTclqyEutDX6DA2pZ7_ABIVugrmEC3XJH92P55_G39GodCmWBFrQJ2PrQAwdLGHig_NxNv9xrQa1dhWIawrv1Wzeqawa8953D-8QOmaEAQAA)): ```svelte
<!--- file: Table.svelte --->
<script>
let { data, header, row } = $props();
</script>
<table>
{#if header}
<thead>
<tr>{@render header()}</tr>
</thead>
{/if}
<tbody>
{#each data as d}
<tr>{@render row(d)}</tr>
{/each}
</tbody>
</table>
<style>
table {
text-align: left;
border-spacing: 0;
}
tbody tr:nth-child(2n+1) {
background: ButtonFace;
}
table :global(th), table :global(td) {
padding: 0.5em;
}
</style>
```
<!-- codeblock:end -->
### Implicit `children` snippet
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet:
<!-- codeblock:start {"title":"Implicit children snippet","selected":"Button.svelte"} -->
```svelte ```svelte
<!--- file: App.svelte ---> <!--- file: App.svelte --->
<script>
import Button from './Button.svelte';
</script>
<Button>click me</Button> <Button>click me</Button>
``` ```
@ -181,9 +285,12 @@ Any content inside the component tags that is _not_ a snippet declaration implic
<!-- result will be <button>click me</button> --> <!-- result will be <button>click me</button> -->
<button>{@render children()}</button> <button>{@render children()}</button>
``` ```
<!-- codeblock:end -->
> [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name > [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name
### Optional snippet props
You can declare snippet props as being optional. You can either use optional chaining to not render anything if the snippet isn't set... You can declare snippet props as being optional. You can either use optional chaining to not render anything if the snippet isn't set...
```svelte ```svelte
@ -248,9 +355,21 @@ We can tighten things up further by declaring a generic, so that `data` and `row
## Exporting snippets ## Exporting snippets
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets) ([demo](/playground/untitled#H4sIAAAAAAAAE3WPwY7CMAxEf8UyB1hRgdhjl13Bga8gHFJipEqtGyUGFUX5dxJUtEB3b9bYM_MckHVLWOKut50TMuC5tpbEY4GnuiGP5T6gXG0-ykLSB8vW2oW_UCNZq7Snv_Rjx0Kc4kpc-6OrrfwoVlK3uQ4CaGMgwsl1LUwXy0f54J9-KV4vf20cNo7YkMu22aqAz4-oOLUI9YKluDPF4h_at-hX5PFyzA1tZ84N3fGpf8YfUU6GvDumLqDKmEqCjjCHUEX4hqDTWCU5PJ6Or38c4g1cPu9tnAEAAA==)): Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets):
<!-- codeblock:start {"title":"Exported snippets","selected":"snippets.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
import { add } from './snippets.svelte';
</script>
{@render add(1, 2)}
```
```svelte ```svelte
<!--- file: snippets.svelte --->
<script module> <script module>
export { add }; export { add };
</script> </script>
@ -259,6 +378,7 @@ Snippets declared at the top level of a `.svelte` file can be exported from a `<
{a} + {b} = {a + b} {a} + {b} = {a + b}
{/snippet} {/snippet}
``` ```
<!-- codeblock:end -->
> [!NOTE] > [!NOTE]
> This requires Svelte 5.5.0 or newer > This requires Svelte 5.5.0 or newer
@ -269,4 +389,4 @@ Snippets can be created programmatically with the [`createRawSnippet`](svelte#cr
## Snippets and slots ## Snippets and slots
In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and as such slots are deprecated in Svelte 5. In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.

@ -1,5 +1,6 @@
--- ---
title: {@html ...} title: {@html ...}
tags: template-html
--- ---
To inject raw HTML into your component, use the `{@html ...}` tag: To inject raw HTML into your component, use the `{@html ...}` tag:
@ -22,7 +23,7 @@ It also will not compile Svelte code.
## Styling ## Styling
Content rendered this way is 'invisible' to Svelte and as such will not receive [scoped styles](scoped-styles) — in other words, this will not work, and the `a` and `img` styles will be regarded as unused: Content rendered this way is 'invisible' to Svelte and as such will not receive [scoped styles](scoped-styles). In other words, this will not work, and the `a` and `img` styles will be regarded as unused:
<!-- prettier-ignore --> <!-- prettier-ignore -->
```svelte ```svelte

@ -0,0 +1,175 @@
---
title: {@attach ...}
tags: attachments
---
Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates.
Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM.
> [!NOTE]
> Attachments are available in Svelte 5.29 and newer.
```svelte
<!--- file: App.svelte --->
<script>
/** @type {import('svelte/attachments').Attachment} */
function myAttachment(element) {
console.log(element.nodeName); // 'DIV'
return () => {
console.log('cleaning up');
};
}
</script>
<div {@attach myAttachment}>...</div>
```
An element can have any number of attachments.
## Attachment factories
A useful pattern is for a function, such as `tooltip` in this example, to _return_ an attachment ([demo](/playground/untitled#H4sIAAAAAAAAE3VT0XLaMBD8lavbDiaNCUlbHhTItG_5h5AH2T5ArdBppDOEMv73SkbGJGnH47F9t3un3TsfMyO3mInsh2SW1Sa7zlZKo8_E0zHjg42pGAjxBPxp7cTvUHOMldLjv-IVGUbDoUw295VTlh-WZslqa8kxsLL2ACtHWxh175NffnQfAAGikSGxYQGfPEvGfPSIWtOH0TiBVo2pWJEBJtKhQp4YYzjG9JIdcuMM5IZqHMPioY8vOSA997zQoevf4a7heO7cdp34olRiTGr07OhwH1IdoO2A7dLMbwahZq6MbRhKZWqxk7rBxTGVbuHmhCgb5qDgmIx_J6XtHHukHTrYYqx_YpzYng8aO4RYayql7hU-1ZJl0akqHBE_D9KLolwL-Dibzc7iSln9XjtqTF1UpMkJ2EmXR-BgQErsN4pxIJKr0RVO1qrxAqaTO4fbc9bKulZm3cfDY3aZDgvFGErWjmzhN7KmfX5rXyDeX8Pt1mU-hXjdBOrtuB97vK4GPUtmJ41XcRMEGDLD8do0nJ73zhUhSlyRw0t3vPqD8cjfLs-axiFgNBrkUd9Ulp50c-GLxlXAVlJX-ffpZyiSn7H0eLCUySZQcQdXlxj4El0Yv_FZvIKElqqGTruVLhzu7VRKCh22_5toOyxsWqLwwzK-cCbYNdg-hy-p9D7sbiZWUnts_wLUOF3CJgQAAA==)):
```svelte
<!--- file: App.svelte --->
<script>
import tippy from 'tippy.js';
let content = $state('Hello!');
/**
* @param {string} content
* @returns {import('svelte/attachments').Attachment}
*/
function tooltip(content) {
return (element) => {
const tooltip = tippy(element, { content });
return tooltip.destroy;
};
}
</script>
<input bind:value={content} />
<button {@attach tooltip(content)}>
Hover me
</button>
```
Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
## Inline attachments
Attachments can also be created inline ([demo](/playground/untitled#H4sIAAAAAAAAE71Wf3OaWBT9KoyTTnW3MS-I3dYmnWXVtnRAazRJzbozRSQEApiRhwKO333vuY8m225m_9yZGOT9OPfcc84D943UTfxGr_G7K6Xr3TVeNW7D2M8avT_3DVk-YAoDNF4vNB8e2tnWjyXGlm7mPzfurVPpp5JgGmeZtwkf5PtFupCxLzVvHa832rl2lElX-s2Xm2DZFNqp_hs-rZetd4v07ORpT3qmQHu7MF2td0BZp8k6z_xkvfXP902_pZ2_1_aYWEiqm0kN8I4r79qbdZ6umnq3q_2iNf22F4dE6qt2oimwdpim_uY6XMm7Fuo-IQT_iTD_CeGTHwZ38ieIJUFQRxirR1Xf39Dw0X5z0I72Af4tD61vvPNwWKQnqmfPTbduhsEd2J3vO_oBd3dc6fF2X7umNdWGf0vBRhSS6qoV7cCXfTXWfKmvWG61_si_vfU92Wz-E4RhsLhNIYinsox9QKGVd8-tuACCeKXRX12P-T_eKf7fhTq0Hvt-f3ailtSeoxJHRo1-58NoPe1UiBc1hkL8Yeh45y_vQ3mcuNl9T8s3cXPRWLnS7YWJG_gn2Tb4tUjid8jua-PVl08j_ab8I14mH8Llx0s5Tz5Err4ql52r_GYg0mVy1bEGZuD0ze64b5TWYFiM-16wSuJ4JT5vfVpDcztrcG_YkRU4s6HxufzDWF4XuVeJ1P10IbzBemt3Vp1V2e04ZXfrJd7Wicyd039brRIv_RIVu_nXi7X1cfL2sy66ztToUp1TO7qJ7NlwZ0f30pld5qNSVE5o6PbMojFHjgZB7oSicPpGteyLclQap7SvY0dXtM_LR1NT2JFHey3aaxa0VxCeYJ7RMHemoiCcgPZV9pR7o7kgcOjeGliYk9hjDZx8FAq6enwlTPSZj_vYPw9Il64dXdIY8ZmapzwfEd8-1ZyaxWhqkIZOibXUd-6Upqi1pD4uMicCV1GA_7zi73UN8BaF4sC8peJtMjfmjbHZBFwq5ov50qRaE0l96NZggnW4KqypYRAW-uhSz9ADvklwJF2J-5W0Z5fQPBhDX92R6I_0IFxRgDftge4l4dP-gH1hjD7uqU6fsOEZ9UNrCdPB-nys6uXgY6O3ZMd9sy5T9PghqrWHdjo4jB51CgLiKJaDYYA-7WgYONf1FbjkI-mE3EAfUY_rijfuJ_CVPaR50oe9JF7Q0pI8Dw3osxxYHdYPGbp2CnwHF8KvwJv2wEv0Z3ilQI6U9uwbZxbYJXvEmjjQjjCHkvNLvNg3yhzXQd1olamsT4IRrZmX0MUDpwL7R8zzHj7pSh9hPHFSHjLezKqAST51uC5zmtQ87skDUaneLokT5RbXkPWSYz53Abgjc8_o4KFGUZ-Hgv2Z1l5OTYM9D-HfUD0L-EwxH5wRnIG61gS-khfgY1bq7IAP_DA4l5xRuh9xlm8yGjutc8t-wHtkhWv3hc7aqGwiK5KzgvM5xRkZYn193uEln-su55j1GaIv7oM4iPrsVHiG0Dx7TR9-1lBfqFdwfvSd5LNL5xyZVp5NoHFZ57FkfiF6vKs4k5zvIfrX5xX6MXmt0gM5MTu8DjnhukrHHzTRd3jm0dma0_f_x5cxP9f4jBdqHvmbq2fUjzqcKh2Cp-yWj9ntcHanXmBXxhu7Q--eyjhfNFpaV7zgz4nWEUb7zUOhpevjjf_gu_KZ99pxFlZ-T3sttkmYqrco_26q35v0Ewzv5EZPbnL_8BfduWGMnyyN3q0bZ_7hb_7KG_L4CQAA)):
```svelte
<!--- file: App.svelte --->
<canvas
width={32}
height={32}
{@attach (canvas) => {
const context = canvas.getContext('2d');
$effect(() => {
context.fillStyle = color;
context.fillRect(0, 0, canvas.width, canvas.height);
});
}}
></canvas>
```
> [!NOTE]
> The nested effect runs whenever `color` changes, while the outer effect (where `canvas.getContext(...)` is called) only runs once, since it doesn't read any reactive state.
## Conditional attachments
Falsy values like `false` or `undefined` are treated as no attachment, enabling conditional usage:
```svelte
<div {@attach enabled && myAttachment}>...</div>
```
## Passing attachments to components
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments.
This allows you to create _wrapper components_ that augment elements ([demo](/playground/untitled#H4sIAAAAAAAAE3VUS3ObMBD-KxvajnFqsJM2PhA7TXrKob31FjITAbKtRkiMtDhJPfz3LiAMdpxhGJvdb1_fPnaeYjn3Iu-WIbJ04028lZDcetHDzsO3olbVApI74F1RhHbLJdayhFl-Sp5qhVwhufEWNjWiwJtYxSjyQhsEFEXxBiujcxg1_8O_dnQ9APwsEbVyiHDafjrvDZCgkiO4MLCEzxYZcn90z6XUZ6OxA61KlaIgV6i1pFC-sxjDrlbHaDiWRoGvdMbHsLzp5DES0mJnRxGaRBvcBHb7yFUTCQeunEWYcYtGv12TqgFUDbCK1WLaM6IWQhUlQiJUFm2ZLPly51xXMG0Rjoyd69C7UqqG2nu95QZyXvtvLVpri2-SN4hoLXXCZFfhQ8aQBU1VgdEaH_vSgyBZR_BpPp_vi0tY-rw2ulRZkGqpTQRbZvwa2BPgFC8bgbw31CbjJjAsE6WNYBZeGp7vtQXLMqHWnZx-5kM1TR5ycpkZXQR2wzL94l8Ur1C_3-g168SfQf1MyfRi3LW9fs77emJEw5QV9SREoLTq06tcczq7d6xEUcJX2vAhO1b843XK34e5unZEMBr15ekuKEusluWAF8lXhE2ZTP2r2RcIHJ-163FPKerCgYJLOB9i4GvNwviI5-gAQiFFBk3tBTOU3HFXEk0R8o86WvUD64aINhv5K3oRmpJXkw8uxMG6Hh6JY9X7OwGSqfUy9tDG3sHNoEi0d_d_fv9qndxRU0VClFqo3KVo3U655Hnt1PXB3Qra2Y2QGdEwgTAMCxopsoxOe6SD0gD8movDhT0LAnhqlE8gVCpLWnRoV7OJCkFAwEXitrYL1W7p7pbiE_P7XH6E_rihODm5s52XtiH9Ekaw0VgI9exadWL1uoEYjPtg2672k5szsxbKyWB2fdT0w5Y_0hcT8oXOlRetmLS8-g-6TLXXQgYAAA==)):
```svelte
<!--- file: Button.svelte --->
<script>
/** @type {import('svelte/elements').HTMLButtonAttributes} */
let { children, ...props } = $props();
</script>
<!-- `props` includes attachments -->
<button {...props}>
{@render children?.()}
</button>
```
```svelte
<!--- file: App.svelte --->
<script>
import tippy from 'tippy.js';
import Button from './Button.svelte';
let content = $state('Hello!');
/**
* @param {string} content
* @returns {import('svelte/attachments').Attachment}
*/
function tooltip(content) {
return (element) => {
const tooltip = tippy(element, { content });
return tooltip.destroy;
};
}
</script>
<input bind:value={content} />
<Button {@attach tooltip(content)}>
Hover me
</Button>
```
## Controlling when attachments re-run
Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
```js
// @errors: 7006 2304 2552
function foo(bar) {
return (node) => {
veryExpensiveSetupWork(node);
update(node, bar);
};
}
```
In the rare case that this is a problem (for example, if `foo` does expensive and unavoidable setup work) consider passing the data inside a function and reading it in a child effect:
```js
// @errors: 7006 2304 2552
function foo(+++getBar+++) {
return (node) => {
veryExpensiveSetupWork(node);
+++ $effect(() => {
update(node, getBar());
});+++
}
}
```
## Creating attachments programmatically
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
## Converting actions to attachments
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components.

@ -2,6 +2,8 @@
title: {@const ...} title: {@const ...}
--- ---
> [!NOTE] `{@const x = y}` is legacy syntax — use [`{const x = $derived(y)}`](declaration-tags) instead
The `{@const ...}` tag defines a local constant. The `{@const ...}` tag defines a local constant.
```svelte ```svelte
@ -11,4 +13,4 @@ The `{@const ...}` tag defines a local constant.
{/each} {/each}
``` ```
`{@const}` is only allowed as an immediate child of a block — `{#if ...}`, `{#each ...}`, `{#snippet ...}` and so on — or a `<Component />`. `{@const}` is only allowed as an immediate child of a block — `{#if ...}`, `{#each ...}`, `{#snippet ...}` and so on — a `<Component />` or a `<svelte:boundary>`.

@ -0,0 +1,72 @@
---
title: {let/const ...}
---
Declaration tags define local variables inside markup with `const` or `let`:
<!-- codeblock:start {"title":"Declaration tags"} -->
```svelte
<!--- file: App.svelte --->
<script>
let boxes = [{ width: 10, height: 10 }, { width: 15, height: 15 }];
</script>
{#each boxes as box}
{const area = box.width * box.height}
{const label = `${box.width} ⨉ ${box.height} = ${area}`}
<p>{label}</p>
{/each}
```
<!-- codeblock:end -->
> [!NOTE] Declaration tags are available since Svelte 5.56.
> [!NOTE] The [`{@const ...}`](@const) syntax is considered legacy — use declaration tags instead.
When values should be reactive, you can use `$state` and `$derived`:
<!-- codeblock:start {"title":"Reactive declaration tags"} -->
```svelte
<!--- file: App.svelte --->
<script>
let user = $state({ name: 'Svelte' });
let editing = $state(false);
</script>
<p>Hello {user.name}</p>
<button onclick={() => editing = true}>edit name</button>
{#if editing}
{let name = $state(user.name)}
{const greeting = $derived(`Hello ${name}`)}
<hr>
<input bind:value={name} />
<p>{greeting}</p>
<button onclick={() => {
user.name = name;
editing = false;
}}>save</button>
{/if}
```
<!-- codeblock:end -->
Declaration tags can be used anywhere inside the component. They can reference values declared outside themselves (for example in the `<script>` tag or in `{#each ...}` blocks) and are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings):
<!-- codeblock:start {"title":"Declaration tag scope"} -->
```svelte
<!--- file: App.svelte --->
{const hello = 'hello'}
{hello} <!-- 'hello' -->
<div>
{const hello = 'hi'}
{hello} <!-- 'hi' -->
<div>
{hello} <!-- 'hi' -->
</div>
</div>
{hello} <!-- 'hello' -->
```
<!-- codeblock:end -->

@ -4,7 +4,7 @@ title: bind:
Data ordinarily flows down, from parent to child. The `bind:` directive allows data to flow the other way, from child to parent. Data ordinarily flows down, from parent to child. The `bind:` directive allows data to flow the other way, from child to parent.
The general syntax is `bind:property={expression}`, where `expression` is an _lvalue_ (i.e. a variable or an object property). When the expression is an identifier with the same name as the property, we can omit the expression — in other words these are equivalent: The general syntax is `bind:property={expression}`, where `expression` is an [_lvalue_](https://press.rebus.community/programmingfundamentals/chapter/lvalue-and-rvalue/) (i.e. a variable or an object property). When the expression is an identifier with the same name as the property, we can omit the expression — in other words these are equivalent:
<!-- prettier-ignore --> <!-- prettier-ignore -->
```svelte ```svelte
@ -54,9 +54,11 @@ A `bind:value` directive on an `<input>` element binds the input's `value` prope
<p>{message}</p> <p>{message}</p>
``` ```
In the case of a numeric input (`type="number"` or `type="range"`), the value will be coerced to a number ([demo](/playground/untitled#H4sIAAAAAAAAE6WPwYoCMQxAfyWEPeyiOOqx2w74Hds9pBql0IllmhGXYf5dKqwiyILsLXnwwsuI-5i4oPkaUX8yo7kCnKNQV7dNzoty4qSVBSr8jG-Poixa0KAt2z5mbb14TaxA4OCtKCm_rz4-f2m403WltrlrYhMFTtcLNkoeFGqZ8yhDF7j3CCHKzpwoDexGmqCL4jwuPUJHZ-dxVcfmyYGe5MAv-La5pbxYFf5Z9Zf_UJXb-sEMquFgJJhBmGyTW5yj8lnRaD_w9D1dAKSSj7zqAQAA)): In the case of a numeric input (`type="number"` or `type="range"`), the value will be coerced to a number:
<!-- codeblock:start {"title":"Numeric bindings"} -->
```svelte ```svelte
<!--- file: App.svelte --->
<script> <script>
let a = $state(1); let a = $state(1);
let b = $state(2); let b = $state(2);
@ -74,6 +76,7 @@ In the case of a numeric input (`type="number"` or `type="range"`), the value wi
<p>{a} + {b} = {a + b}</p> <p>{a} + {b} = {a + b}</p>
``` ```
<!-- codeblock:end -->
If the input is empty or invalid (in the case of `type="number"`), the value is `undefined`. If the input is empty or invalid (in the case of `type="number"`), the value is `undefined`.
@ -95,7 +98,7 @@ Since 5.6.0, if an `<input>` has a `defaultValue` and is part of a form, it will
## `<input bind:checked>` ## `<input bind:checked>`
Checkbox and radio inputs can be bound with `bind:checked`: Checkbox inputs can be bound with `bind:checked`:
```svelte ```svelte
<label> <label>
@ -117,29 +120,68 @@ Since 5.6.0, if an `<input>` has a `defaultChecked` attribute and is part of a f
</form> </form>
``` ```
> [!NOTE] Use `bind:group` for radio inputs instead of `bind:checked`.
## `<input bind:indeterminate>`
Checkboxes can be in an [indeterminate](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/indeterminate) state, independently of whether they are checked or unchecked:
```svelte
<script>
let checked = $state(false);
let indeterminate = $state(true);
</script>
<form>
<input type="checkbox" bind:checked bind:indeterminate>
{#if indeterminate}
waiting...
{:else if checked}
checked
{:else}
unchecked
{/if}
</form>
```
## `<input bind:group>` ## `<input bind:group>`
Inputs that work together can use `bind:group`. Inputs that work together can use `bind:group`:
<!-- codeblock:start {"title":"bind:group"} -->
```svelte ```svelte
<!--- file: App.svelte --->
<script> <script>
let tortilla = $state('Plain'); let tortilla = $state('Plain');
/** @type {Array<string>} */ /** @type {string[]} */
let fillings = $state([]); let fillings = $state([]);
</script> </script>
<h1>Customize your burrito</h1>
<!-- grouped radio inputs are mutually exclusive --> <!-- grouped radio inputs are mutually exclusive -->
<input type="radio" bind:group={tortilla} value="Plain" /> <label><input type="radio" bind:group={tortilla} value="Plain" /> Plain</label>
<input type="radio" bind:group={tortilla} value="Whole wheat" /> <label><input type="radio" bind:group={tortilla} value="Whole wheat" /> Whole wheat</label>
<input type="radio" bind:group={tortilla} value="Spinach" /> <label><input type="radio" bind:group={tortilla} value="Spinach" /> Spinach</label>
<!-- grouped checkbox inputs populate an array --> <!-- grouped checkbox inputs populate an array -->
<input type="checkbox" bind:group={fillings} value="Rice" /> <label><input type="checkbox" bind:group={fillings} value="Rice" /> Rice</label>
<input type="checkbox" bind:group={fillings} value="Beans" /> <label><input type="checkbox" bind:group={fillings} value="Beans" /> Beans</label>
<input type="checkbox" bind:group={fillings} value="Cheese" /> <label><input type="checkbox" bind:group={fillings} value="Cheese" /> Cheese</label>
<input type="checkbox" bind:group={fillings} value="Guac (extra)" /> <label><input type="checkbox" bind:group={fillings} value="Guac (extra)" /> Guac (extra)</label>
<p>Tortilla: {tortilla}</p>
<p>Fillings: {fillings.join(', ') || 'None'}</p>
<style>
label {
display: block;
}
</style>
``` ```
<!-- codeblock:end -->
> [!NOTE] `bind:group` only works if the inputs are in the same Svelte component. > [!NOTE] `bind:group` only works if the inputs are in the same Svelte component.
@ -219,15 +261,15 @@ You can give the `<select>` a default value by adding a `selected` attribute to
- [`volume`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/volume) - [`volume`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/volume)
- [`muted`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/muted) - [`muted`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/muted)
...and seven readonly ones: ...and six readonly ones:
- [`duration`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/duration) - [`duration`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/duration)
- [`buffered`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/buffered) - [`buffered`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/buffered)
- [`paused`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/paused)
- [`seekable`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seekable) - [`seekable`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seekable)
- [`seeking`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seeking_event) - [`seeking`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seeking_event)
- [`ended`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended) - [`ended`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended)
- [`readyState`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState) - [`readyState`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState)
- [`played`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/played)
```svelte ```svelte
<audio src={clip} bind:duration bind:currentTime bind:paused></audio> <audio src={clip} bind:duration bind:currentTime bind:paused></audio>
@ -235,7 +277,7 @@ You can give the `<select>` a default value by adding a `selected` attribute to
## `<video>` ## `<video>`
`<video>` elements have all the same bindings as [#audio] elements, plus readonly [`videoWidth`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/videoWidth) and [`videoHeight`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/videoHeight) bindings. `<video>` elements have all the same bindings as [`<audio>`](#audio) elements, plus readonly [`videoWidth`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/videoWidth) and [`videoHeight`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/videoHeight) bindings.
## `<img>` ## `<img>`
@ -255,6 +297,10 @@ You can give the `<select>` a default value by adding a `selected` attribute to
</details> </details>
``` ```
## `window` and `document`
To bind to properties of `window` and `document`, see [`<svelte:window>`](svelte-window) and [`<svelte:document>`](svelte-document).
## Contenteditable bindings ## Contenteditable bindings
Elements with the `contenteditable` attribute support the following bindings: Elements with the `contenteditable` attribute support the following bindings:
@ -268,7 +314,7 @@ Elements with the `contenteditable` attribute support the following bindings:
<!-- for some reason puts the comment and html on same line --> <!-- for some reason puts the comment and html on same line -->
<!-- prettier-ignore --> <!-- prettier-ignore -->
```svelte ```svelte
<div contenteditable="true" bind:innerHTML={html} /> <div contenteditable="true" bind:innerHTML={html}></div>
``` ```
## Dimensions ## Dimensions
@ -279,6 +325,10 @@ All visible elements have the following readonly bindings, measured with a `Resi
- [`clientHeight`](https://developer.mozilla.org/en-US/docs/Web/API/Element/clientHeight) - [`clientHeight`](https://developer.mozilla.org/en-US/docs/Web/API/Element/clientHeight)
- [`offsetWidth`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth) - [`offsetWidth`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth)
- [`offsetHeight`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight) - [`offsetHeight`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight)
- [`contentRect`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/contentRect)
- [`contentBoxSize`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/contentBoxSize)
- [`borderBoxSize`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/borderBoxSize)
- [`devicePixelContentBoxSize`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/devicePixelContentBoxSize)
```svelte ```svelte
<div bind:offsetWidth={width} bind:offsetHeight={height}> <div bind:offsetWidth={width} bind:offsetHeight={height}>
@ -286,7 +336,7 @@ All visible elements have the following readonly bindings, measured with a `Resi
</div> </div>
``` ```
> [!NOTE] `display: inline` elements do not have a width or height (except for elements with 'intrinsic' dimensions, like `<img>` and `<canvas>`), and cannot be observed with a `ResizeObserver`. You will need to change the `display` style of these elements to something else, such as `inline-block`. > [!NOTE] `display: inline` elements do not have a width or height (except for elements with 'intrinsic' dimensions, like `<img>` and `<canvas>`), and cannot be observed with a `ResizeObserver`. You will need to change the `display` style of these elements to something else, such as `inline-block`. Note that CSS transformations do not trigger `ResizeObserver` callbacks.
## bind:this ## bind:this
@ -308,7 +358,7 @@ To get a reference to a DOM node, use `bind:this`. The value will be `undefined`
}); });
</script> </script>
<canvas bind:this={canvas} /> <canvas bind:this={canvas}></canvas>
``` ```
Components also support `bind:this`, allowing you to interact with component instances programmatically. Components also support `bind:this`, allowing you to interact with component instances programmatically.
@ -330,6 +380,8 @@ Components also support `bind:this`, allowing you to interact with component ins
</script> </script>
``` ```
> [!NOTE] In case of using [the function bindings](#Function-bindings), the getter is required to ensure that the correct value is nullified on component or element destruction.
## bind:_property_ for components ## bind:_property_ for components
```svelte ```svelte

@ -2,6 +2,9 @@
title: use: title: use:
--- ---
> [!NOTE]
> In Svelte 5.29 and newer, consider using [attachments](@attach) instead, as they are more flexible and composable.
Actions are functions that are called when an element is mounted. They are added with the `use:` directive, and will typically use an `$effect` so that they can reset any state when the element is unmounted: Actions are functions that are called when an element is mounted. They are added with the `use:` directive, and will typically use an `$effect` so that they can reset any state when the element is unmounted:
```svelte ```svelte
@ -50,17 +53,16 @@ The `Action` interface receives three optional type arguments — a node type (w
```svelte ```svelte
<!--- file: App.svelte ---> <!--- file: App.svelte --->
<script> <script>
import { on } from 'svelte/events';
/** /**
* @type {import('svelte/action').Action< * @type {import('svelte/action').Action<
* HTMLDivElement, * HTMLDivElement,
* null, * undefined,
* { * {
* onswiperight: (e: CustomEvent) => void; * onswiperight: (e: CustomEvent) => void;
* onswipeleft: (e: CustomEvent) => void; * onswipeleft: (e: CustomEvent) => void;
* // ... * // ...
* }>} * }
* >}
*/ */
function gestures(node) { function gestures(node) {
$effect(() => { $effect(() => {

@ -1,5 +1,6 @@
--- ---
title: transition: title: transition:
tags: transitions
--- ---
A _transition_ is triggered by an element entering or leaving the DOM as a result of a state change. A _transition_ is triggered by an element entering or leaving the DOM as a result of a state change.
@ -22,10 +23,6 @@ The `transition:` directive indicates a _bidirectional_ transition, which means
{/if} {/if}
``` ```
## Built-in transitions
A selection of built-in transitions can be imported from the [`svelte/transition`](svelte-transition) module.
## Local vs global ## Local vs global
Transitions are local by default. Local transitions only play when the block they belong to is created or destroyed, _not_ when parent blocks are created or destroyed. Transitions are local by default. Local transitions only play when the block they belong to is created or destroyed, _not_ when parent blocks are created or destroyed.
@ -40,6 +37,10 @@ Transitions are local by default. Local transitions only play when the block the
{/if} {/if}
``` ```
## Built-in transitions
A selection of built-in transitions can be imported from the [`svelte/transition`](svelte-transition) module.
## Transition parameters ## Transition parameters
Transitions can have parameters. Transitions can have parameters.

@ -1,5 +1,6 @@
--- ---
title: in: and out: title: in: and out:
tags: transitions
--- ---
The `in:` and `out:` directives are identical to [`transition:`](transition), except that the resulting transitions are not bidirectional — an `in` transition will continue to 'play' alongside the `out` transition, rather than reversing, if the block is outroed while the transition is in progress. If an out transition is aborted, transitions will restart from scratch. The `in:` and `out:` directives are identical to [`transition:`](transition), except that the resulting transitions are not bidirectional — an `in` transition will continue to 'play' alongside the `out` transition, rather than reversing, if the block is outroed while the transition is in progress. If an out transition is aborted, transitions will restart from scratch.

@ -1,23 +0,0 @@
---
title: class:
---
The `class:` directive is a convenient way to conditionally set classes on elements, as an alternative to using conditional expressions inside `class` attributes:
```svelte
<!-- These are equivalent -->
<div class={isCool ? 'cool' : ''}>...</div>
<div class:cool={isCool}>...</div>
```
As with other directives, we can use a shorthand when the name of the class coincides with the value:
```svelte
<div class:cool>...</div>
```
Multiple `class:` directives can be added to a single element:
```svelte
<div class:cool class:lame={!cool} class:potato>...</div>
```

@ -1,5 +1,6 @@
--- ---
title: style: title: style:
tags: template-style
--- ---
The `style:` directive provides a shorthand for setting multiple styles on an element. The `style:` directive provides a shorthand for setting multiple styles on an element.
@ -34,8 +35,16 @@ To mark a style as important, use the `|important` modifier:
<div style:color|important="red">...</div> <div style:color|important="red">...</div>
``` ```
When `style:` directives are combined with `style` attributes, the directives will take precedence: When `style:` directives are combined with `style` attributes, the directives will take precedence,
even over `!important` properties:
```svelte ```svelte
<div style="color: blue;" style:color="red">This will be red</div> <div style:color="red" style="color: blue">This will be red</div>
<div style:color="red" style="color: blue !important">This will still be red</div>
```
You can set CSS custom properties:
```svelte
<div style:--columns={columns}>...</div>
``` ```

@ -0,0 +1,103 @@
---
title: class
tags: template-style
---
There are two ways to set classes on elements: the `class` attribute, and the `class:` directive.
## Attributes
Primitive values are treated like any other attribute:
```svelte
<div class={large ? 'large' : 'small'}>...</div>
```
> [!NOTE]
> For historical reasons, falsy values (like `false` and `NaN`) are stringified (`class="false"`), though `class={undefined}` (or `null`) cause the attribute to be omitted altogether. In a future version of Svelte, all falsy values will cause `class` to be omitted.
### Objects and arrays
Since Svelte 5.16, `class` can be an object or array, and is converted to a string using [clsx](https://github.com/lukeed/clsx).
If the value is an object, the truthy keys are added:
```svelte
<script>
let { cool } = $props();
</script>
<!-- results in `class="cool"` if `cool` is truthy,
`class="lame"` otherwise -->
<div class={{ cool, lame: !cool }}>...</div>
```
If the value is an array, the truthy values are combined:
```svelte
<!-- if `faded` and `large` are both truthy, results in
`class="saturate-0 opacity-50 scale-200"` -->
<div class={[faded && 'saturate-0 opacity-50', large && 'scale-200']}>...</div>
```
Note that whether we're using the array or object form, we can set multiple classes simultaneously with a single condition, which is particularly useful if you're using things like Tailwind.
Arrays can contain arrays and objects, and clsx will flatten them. This is useful for combining local classes with props, for example:
```svelte
<!--- file: Button.svelte --->
<script>
let props = $props();
</script>
<button {...props} class={['cool-button', props.class]}>
{@render props.children?.()}
</button>
```
The user of this component has the same flexibility to use a mixture of objects, arrays and strings:
```svelte
<!--- file: App.svelte --->
<script>
import Button from './Button.svelte';
let useTailwind = $state(false);
</script>
<Button
onclick={() => useTailwind = true}
class={{ 'bg-blue-700 sm:w-1/2': useTailwind }}
>
Accept the inevitability of Tailwind
</Button>
```
Since Svelte 5.19, Svelte also exposes the `ClassValue` type, which is the type of value that the `class` attribute on elements accept. This is useful if you want to use a type-safe class name in component props:
```svelte
<script lang="ts">
import type { ClassValue } from 'svelte/elements';
const props: { class: ClassValue } = $props();
</script>
<div class={['original', props.class]}>...</div>
```
## The `class:` directive
Prior to Svelte 5.16, the `class:` directive was the most convenient way to set classes on elements conditionally.
```svelte
<!-- These are equivalent -->
<div class={{ cool, lame: !cool }}>...</div>
<div class:cool={cool} class:lame={!cool}>...</div>
```
As with other directives, we can use a shorthand when the name of the class coincides with the value:
```svelte
<div class:cool class:lame={!cool}>...</div>
```
> [!NOTE] Unless you're using an older version of Svelte, consider avoiding `class:`, since the attribute is more powerful and composable.

@ -0,0 +1,200 @@
---
title: await
---
As of Svelte 5.36, you can use the `await` keyword inside your components in three places where it was previously unavailable:
- at the top level of your component's `<script>`
- inside `$derived(...)` declarations
- inside your markup
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](/docs/kit/configuration) Svelte, usually `svelte.config.js`:
```js
/// file: svelte.config.js
export default {
compilerOptions: {
experimental: {
async: true
}
}
};
```
The experimental flag will be removed in Svelte 6.
## Synchronized updates
When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like this...
<!-- codeblock:start {"title":"Synchronized updates"} -->
```svelte
<!--- file: App.svelte --->
<script>
let a = $state(1);
let b = $state(2);
async function add(a, b) {
await new Promise((f) => setTimeout(f, 500)); // artificial delay
return a + b;
}
</script>
<input type="number" bind:value={a}>
<input type="number" bind:value={b}>
<p>{a} + {b} = {await add(a, b)}</p>
```
<!-- codeblock:end -->
...if you increment `a`, the contents of the `<p>` will _not_ immediately update to read this —
```html
<p>2 + 2 = 3</p>
```
— instead, the text will update to `2 + 2 = 4` when `add(a, b)` resolves.
Updates can overlap — a fast update will be reflected in the UI while an earlier slow update is still ongoing.
## Concurrency
Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup...
```svelte
<p>{await one(x)}</p>
<p>{await two(y)}</p>
```
...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential.
This does not apply to sequential `await` expressions inside your `<script>` or inside async functions — these run like any other asynchronous JavaScript. An exception is that independent `$derived` expressions will update independently, even though they will run sequentially when they are first created:
```js
/** @param {number} x */
async function one(x) { return x; }
/** @param {number} y */
async function two(y) { return y; }
let x = $state(1);
let y = $state(2);
// ---cut---
// `b` will not be created until `a` has resolved,
// but once created they will update independently
// even if `x` and `y` update simultaneously
let a = $derived(await one(x));
let b = $derived(await two(y));
```
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](runtime-warnings#Client-warnings-await_waterfall) warning
## Indicating loading states
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](svelte-boundary#Properties-pending) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`]($effect#$effect.pending). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
You can also use [`settled()`](svelte#settled) to get a promise that resolves when the current update is complete:
```js
let color = 'red';
let answer = -1;
let updating = false;
// ---cut---
import { tick, settled } from 'svelte';
async function onclick() {
updating = true;
// without this, the change to `updating` will be
// grouped with the other changes, meaning it
// won't be reflected in the UI
await tick();
color = 'octarine';
answer = 42;
await settled();
// any updates affected by `color` or `answer`
// have now been applied
updating = false;
}
```
## Error handling
Errors in `await` expressions will bubble to the nearest [error boundary](svelte-boundary).
## Server-side rendering
Svelte supports asynchronous server-side rendering (SSR) with the `render(...)` API. To use it, simply await the return value:
```js
/// file: server.js
import { render } from 'svelte/server';
import App from './App.svelte';
const { head, body } = +++await+++ render(App);
```
> [!NOTE] If you're using a framework like SvelteKit, this is done on your behalf.
If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, that snippet will be rendered while the rest of the content is ignored. All `await` expressions encountered outside boundaries with `pending` snippets will resolve and render their contents prior to `await render(...)` returning.
> [!NOTE] In the future, we plan to add a streaming implementation that renders the content in the background.
## Forking
The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.
```svelte
<script>
import { fork } from 'svelte';
import Menu from './Menu.svelte';
let open = $state(false);
/** @type {import('svelte').Fork | null} */
let pending = null;
function preload() {
pending ??= fork(() => {
open = true;
});
}
function discard() {
pending?.discard();
pending = null;
}
</script>
<button
onfocusin={preload}
onfocusout={discard}
onpointerenter={preload}
onpointerleave={discard}
onclick={() => {
pending?.commit();
pending = null;
// in case `pending` didn't exist
// (if it did, this is a no-op)
open = true;
}}
>open menu</button>
{#if open}
<!-- any async work inside this component will start
as soon as the fork is created -->
<Menu onclose={() => open = false} />
{/if}
```
## Caveats
As an experimental feature, the details of how `await` is handled (and related APIs like `$effect.pending()`) are subject to breaking changes outside of a semver major release, though we intend to keep such changes to a bare minimum.
## Breaking changes
Effects run in a slightly different order when the `experimental.async` option is `true`. Specifically, _block_ effects like `{#if ...}` and `{#each ...}` now run before an `$effect.pre` or `beforeUpdate` in the same component, which means that in [very rare situations](/playground/untitled?#H4sIAAAAAAAAE22R3VLDIBCFX2WLvUhnTHsf0zre-Q7WmfwtFV2BgU1rJ5N3F0jaOuoVcPbw7VkYhK4_URTiGYkMnIyjDjLsFGO3EvdCKkIvipdB8NlGXxSCPt96snbtj0gctab2-J_eGs2oOWBE6VunLO_2es-EDKZ5x5ZhC0vPNWM2gHXGouNzAex6hHH1cPHil_Lsb95YT9VQX6KUAbS2DrNsBdsdDFHe8_XSYjH1SrhELTe3MLpsemajweiWVPuxHSbKNd-8eQTdE0EBf4OOaSg2hwNhhE_ABB_ulJzjj9FULvIcqgm5vnAqUB7wWFMfhuugQWkcAr8hVD-mq8D12kOep24J_IszToOXdveGDsuNnZwbJUNlXsKnhJdhUcTo42s41YpOSneikDV5HL8BktM6yRcCAAA=) it is possible to update a block that should no longer exist, but only if you update state inside an effect, [which you should avoid]($effect#When-not-to-use-$effect).

@ -1,111 +0,0 @@
---
title: Control flow
---
- if
- each
- await (or move that into some kind of data loading section?)
- NOT: key (move into transition section, because that's the common use case)
Svelte augments HTML with control flow blocks to be able to express conditionally rendered content or lists.
The syntax between these blocks is the same:
- `{#` denotes the start of a block
- `{:` denotes a different branch part of the block. Depending on the block, there can be multiple of these
- `{/` denotes the end of a block
## {#if ...}
## {#each ...}
```svelte
<!--- copy: false --->
{#each expression as name}...{/each}
```
```svelte
<!--- copy: false --->
{#each expression as name, index}...{/each}
```
```svelte
<!--- copy: false --->
{#each expression as name (key)}...{/each}
```
```svelte
<!--- copy: false --->
{#each expression as name, index (key)}...{/each}
```
```svelte
<!--- copy: false --->
{#each expression as name}...{:else}...{/each}
```
Iterating over lists of values can be done with an each block.
```svelte
<h1>Shopping list</h1>
<ul>
{#each items as item}
<li>{item.name} x {item.qty}</li>
{/each}
</ul>
```
You can use each blocks to iterate over any array or array-like value — that is, any object with a `length` property.
An each block can also specify an _index_, equivalent to the second argument in an `array.map(...)` callback:
```svelte
{#each items as item, i}
<li>{i + 1}: {item.name} x {item.qty}</li>
{/each}
```
If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to diff the list when data changes, rather than adding or removing items at the end. The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change.
```svelte
{#each items as item (item.id)}
<li>{item.name} x {item.qty}</li>
{/each}
<!-- or with additional index value -->
{#each items as item, i (item.id)}
<li>{i + 1}: {item.name} x {item.qty}</li>
{/each}
```
You can freely use destructuring and rest patterns in each blocks.
```svelte
{#each items as { id, name, qty }, i (id)}
<li>{i + 1}: {name} x {qty}</li>
{/each}
{#each objects as { id, ...rest }}
<li><span>{id}</span><MyComponent {...rest} /></li>
{/each}
{#each items as [id, ...rest]}
<li><span>{id}</span><MyComponent values={rest} /></li>
{/each}
```
An each block can also have an `{:else}` clause, which is rendered if the list is empty.
```svelte
{#each todos as todo}
<p>{todo.text}</p>
{:else}
<p>No tasks today!</p>
{/each}
```
It is possible to iterate over iterables like `Map` or `Set`. Iterables need to be finite and static (they shouldn't change while being iterated over). Under the hood, they are transformed to an array using `Array.from` before being passed off to rendering. If you're writing performance-sensitive code, try to avoid iterables and use regular arrays as they are more performant.
## Other block types
Svelte also provides [`#snippet`](snippets), [`#key`](transitions-and-animations) and [`#await`](data-fetching) blocks. You can find out more about them in their respective sections.

@ -1,20 +0,0 @@
---
title: Data fetching
---
Fetching data is a fundamental part of apps interacting with the outside world. Svelte is unopinionated with how you fetch your data. The simplest way would be using the built-in `fetch` method:
```svelte
<script>
let response = $state();
fetch('/api/data').then(async (r) => (response = r.json()));
</script>
```
While this works, it makes working with promises somewhat unergonomic. Svelte alleviates this problem using the `#await` block.
## {#await ...}
## SvelteKit loaders
Fetching inside your components is great for simple use cases, but it's prone to data loading waterfalls and makes code harder to work with because of the promise handling. SvelteKit solves this problem by providing a opinionated data loading story that is coupled to its router. Learn more about it [in the docs](../kit).

@ -1,5 +1,6 @@
--- ---
title: Scoped styles title: Scoped styles
tags: styles-scoped
--- ---
Svelte components can include a `<style>` element containing CSS that belongs to the component. This CSS is _scoped_ by default, meaning that styles will not apply to any elements on the page outside the component in question. Svelte components can include a `<style>` element containing CSS that belongs to the component. This CSS is _scoped_ by default, meaning that styles will not apply to any elements on the page outside the component in question.
@ -33,10 +34,7 @@ If a component defines `@keyframes`, the name is scoped to the component using t
/* these keyframes are only accessible inside this component */ /* these keyframes are only accessible inside this component */
@keyframes bounce { @keyframes bounce {
/* ... *. /* ... */
} }
</style> </style>
``` ```

@ -1,5 +1,6 @@
--- ---
title: Global styles title: Global styles
tags: styles-global
--- ---
## :global(...) ## :global(...)

@ -1,5 +1,6 @@
--- ---
title: Custom properties title: Custom properties
tags: styles-custom-properties
--- ---
You can pass CSS custom properties — both static and dynamic — to components: You can pass CSS custom properties — both static and dynamic — to components:

@ -9,19 +9,41 @@ title: <svelte:boundary>
> [!NOTE] > [!NOTE]
> This feature was added in 5.3.0 > This feature was added in 5.3.0
Boundaries allow you to guard against errors in part of your app from breaking the app as a whole, and to recover from those errors. Boundaries allow you to 'wall off' parts of your app, so that you can:
If an error occurs while rendering or updating the children of a `<svelte:boundary>`, or running any [`$effect`]($effect) functions contained therein, the contents will be removed. - provide UI that should be shown when [`await`](await-expressions) expressions are first resolving
- handle errors that occur during rendering or while running effects, and provide UI that should be rendered when an error happens
Errors occurring outside the rendering process (for example, in event handlers) are _not_ caught by error boundaries. If a boundary handles an error (with a `failed` snippet or `onerror` handler, or both) its existing content will be removed.
> [!NOTE] Errors occurring outside the rendering process (for example, in event handlers or after a `setTimeout` or async work) are _not_ caught by error boundaries.
## Properties ## Properties
For the boundary to do anything, one or both of `failed` and `onerror` must be provided. For the boundary to do anything, one or more of the following must be provided.
### `pending`
This snippet will be shown when the boundary is first created, and will remain visible until all the [`await`](await-expressions) expressions inside the boundary have resolved ([demo](/playground/untitled#H4sIAAAAAAAAE21QQW6DQAz8ytY9BKQVpFdKkPqDHnorPWzAaSwt3tWugUaIv1eE0KpKD5as8YxnNBOw6RAKKOOAVrA4up5bEy6VGknOyiO3xJ8qMnmPAhpOZDFC8T6BXPyiXADQ258X77P1FWg4moj_4Y1jQZZ49W0CealqruXUcyPkWLVozQXbZDC2R606spYiNo7bqA7qab_fp2paFLUElD6wYhzVa3AdRUySgNHZAVN1qDZaLRHljTp0vSTJ9XJjrSbpX5f0eZXN6zLXXOa_QfmurIVU-moyoyH5ib87o7XuYZfOZe6vnGWmx1uZW7lJOq9upa-sMwuUZdkmmfIbfQ1xZwwaBL8ECgk9zh8axJAdiVsoTsZGnL8Bg4tX_OMBAAA=)):
```svelte
<svelte:boundary>
<p>{await delayed('hello!')}</p>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
```
The `pending` snippet will _not_ be shown for subsequent async updates — for these, you can use [`$effect.pending()`]($effect#$effect.pending).
> [!NOTE] In the [playground](/playground), your app is rendered inside a boundary with an empty pending snippet, so that you can use `await` without having to create one.
### `failed` ### `failed`
If a `failed` snippet is provided, it will be rendered with the error that was thrown, and a `reset` function that recreates the contents ([demo](/playground/hello-world#H4sIAAAAAAAAE3VRy26DMBD8lS2tFCIh6JkAUlWp39Cq9EBg06CAbdlLArL87zWGKk8ORnhmd3ZnrD1WtOjFXqKO2BDGW96xqpBD5gXerm5QefG39mgQY9EIWHxueRMinLosti0UPsJLzggZKTeilLWgLGc51a3gkuCjKQ7DO7cXZotgJ3kLqzC6hmex1SZnSXTWYHcrj8LJjWTk0PHoZ8VqIdCOKayPykcpuQxAokJaG1dGybYj4gw4K5u6PKTasSbjXKgnIDlA8VvUdo-pzonraBY2bsH7HAl78mKSHZpgIcuHjq9jXSpZSLixRlveKYQUXhQVhL6GPobXAAb7BbNeyvNUs4qfRg3OnELLj5hqH9eQZqCnoBwR9lYcQxuVXeBzc8kMF8yXY4yNJ5oGiUzP_aaf_waTRGJib5_Ad3P_vbCuaYxzeNpbU0eUMPAOKh7Yw1YErgtoXyuYlPLzc10_xo_5A91zkQL_AgAA)): If a `failed` snippet is provided, it will be rendered when an error is thrown inside the boundary, with the `error` and a `reset` function that recreates the contents ([demo](/playground/hello-world#H4sIAAAAAAAAE3VRy26DMBD8lS2tFCIh6JkAUlWp39Cq9EBg06CAbdlLArL87zWGKk8ORnhmd3ZnrD1WtOjFXqKO2BDGW96xqpBD5gXerm5QefG39mgQY9EIWHxueRMinLosti0UPsJLzggZKTeilLWgLGc51a3gkuCjKQ7DO7cXZotgJ3kLqzC6hmex1SZnSXTWYHcrj8LJjWTk0PHoZ8VqIdCOKayPykcpuQxAokJaG1dGybYj4gw4K5u6PKTasSbjXKgnIDlA8VvUdo-pzonraBY2bsH7HAl78mKSHZpgIcuHjq9jXSpZSLixRlveKYQUXhQVhL6GPobXAAb7BbNeyvNUs4qfRg3OnELLj5hqH9eQZqCnoBwR9lYcQxuVXeBzc8kMF8yXY4yNJ5oGiUzP_aaf_waTRGJib5_Ad3P_vbCuaYxzeNpbU0eUMPAOKh7Yw1YErgtoXyuYlPLzc10_xo_5A91zkQL_AgAA)):
```svelte ```svelte
<svelte:boundary> <svelte:boundary>
@ -80,3 +102,40 @@ If an `onerror` function is provided, it will be called with the same two `error
``` ```
If an error occurs inside the `onerror` function (or if you rethrow the error), it will be handled by a parent boundary if such exists. If an error occurs inside the `onerror` function (or if you rethrow the error), it will be handled by a parent boundary if such exists.
## Using `transformError`
By default, error boundaries have no effect on the server — if an error occurs during rendering, the render as a whole will fail.
Since 5.51 you can control this behaviour for boundaries with a `failed` snippet, by calling [`render(...)`](imperative-component-api#render) with a `transformError` function.
> [!NOTE] If you're using Svelte via a framework such as SvelteKit, you most likely don't have direct access to the `render(...)` call — the framework must configure `transformError` on your behalf. SvelteKit will add support for this in the near future, via the [`handleError`](../kit/hooks#Shared-hooks-handleError) hook.
The `transformError` function must return a JSON-stringifiable object which will be used to render the `failed` snippet. This object will be serialized and used to hydrate the snippet in the browser:
```js
// @errors: 1005
import { render } from 'svelte/server';
import App from './App.svelte';
const { head, body } = await render(App, {
transformError: (error) => {
// log the original error, with the stack trace...
console.error(error);
// ...and return a sanitized user-friendly error
// to display in the `failed` snippet
return {
message: 'An error occurred!'
};
};
});
```
If `transformError` throws (or rethrows) an error, `render(...)` as a whole will fail with that error.
> [!NOTE] Errors that occur during server-side rendering can contain sensitive information in the `message` and `stack`. It's recommended to redact these rather than sending them unaltered to the browser.
If the boundary has an `onerror` handler, it will be called upon hydration with the deserialized error object.
The [`mount`](imperative-component-api#mount) and [`hydrate`](imperative-component-api#hydrate) functions also accept a `transformError` option, which defaults to the identity function. As with `render`, this function transforms a render-time error before it is passed to a `failed` snippet or `onerror` handler.

@ -10,12 +10,12 @@ title: <svelte:document>
<svelte:document bind:prop={value} /> <svelte:document bind:prop={value} />
``` ```
Similarly to `<svelte:window>`, this element allows you to add listeners to events on `document`, such as `visibilitychange`, which don't fire on `window`. It also lets you use [actions](use) on `document`. Similarly to `<svelte:window>`, this element allows you to add listeners to events on `document`, such as `visibilitychange`, which don't fire on `window`. It also lets you use [attachments](@attach) on `document`.
As with `<svelte:window>`, this element may only appear the top level of your component and must never be inside a block or element. As with `<svelte:window>`, this element may only appear the top level of your component and must never be inside a block or element.
```svelte ```svelte
<svelte:document onvisibilitychange={handleVisibilityChange} use:someAction /> <svelte:document onvisibilitychange={handleVisibilityChange} {@attach someAttachment} />
``` ```
You can also bind to the following properties: You can also bind to the following properties:

@ -8,7 +8,7 @@ title: <svelte:body>
Similarly to `<svelte:window>`, this element allows you to add listeners to events on `document.body`, such as `mouseenter` and `mouseleave`, which don't fire on `window`. It also lets you use [actions](use) on the `<body>` element. Similarly to `<svelte:window>`, this element allows you to add listeners to events on `document.body`, such as `mouseenter` and `mouseleave`, which don't fire on `window`. It also lets you use [actions](use) on the `<body>` element.
As with `<svelte:window>` and `<svelte:document>`, this element may only appear the top level of your component and must never be inside a block or element. As with `<svelte:window>` and `<svelte:document>`, this element may only appear at the top level of your component and must never be inside a block or element.
```svelte ```svelte
<svelte:body onmouseenter={handleMouseenter} onmouseleave={handleMouseleave} use:someAction /> <svelte:body onmouseenter={handleMouseenter} onmouseleave={handleMouseleave} use:someAction />

@ -12,7 +12,7 @@ The `<svelte:options>` element provides a place to specify per-component compile
- `runes={false}` — forces a component into _legacy mode_ - `runes={false}` — forces a component into _legacy mode_
- `namespace="..."` — the namespace where this component will be used, can be "html" (the default), "svg" or "mathml" - `namespace="..."` — the namespace where this component will be used, can be "html" (the default), "svg" or "mathml"
- `customElement={...}` — the [options](custom-elements#Component-options) to use when compiling this component as a custom element. If a string is passed, it is used as the `tag` option - `customElement={...}` — the [options](custom-elements#Component-options) to use when compiling this component as a custom element. If a string is passed, it is used as the `tag` option
- `css="injected"` — the component will inject its styles inline: During server side rendering, it's injected as a `<style>` tag in the `head`, during client side rendering, it's loaded via JavaScript - `css="injected"` — the component will inject its styles inline: During server-side rendering, it's injected as a `<style>` tag in the `head`, during client side rendering, it's loaded via JavaScript
> [!LEGACY] Deprecated options > [!LEGACY] Deprecated options
> Svelte 4 also included the following options. They are deprecated in Svelte 5 and non-functional in runes mode. > Svelte 4 also included the following options. They are deprecated in Svelte 5 and non-functional in runes mode.

@ -2,129 +2,224 @@
title: Context title: Context
--- ---
<!-- - get/set/hasContext Context allows components to access values owned by parent components without passing them down as props (potentially through many layers of intermediate components, known as 'prop-drilling').
- how to use, best practises (like encapsulating them) -->
Most state is component-level state that lives as long as its component lives. There's also section-wide or app-wide state however, which also needs to be handled somehow. By creating a `[get, set]` pair of functions with `createContext`, you can set the context in a parent component and get it in a child component:
The easiest way to do that is to create global state and just import that. <!-- codeblock:start {"title":"Context","selected":"context.ts"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Parent from './Parent.svelte';
import Child from './Child.svelte';
</script>
```ts <Parent>
/// file: state.svelte.js <Child />
export const myGlobalState = $state({ </Parent>
user: {
/* ... */
}
/* ... */
});
``` ```
```svelte ```svelte
<!--- file: App.svelte ---> <!--- file: Parent.svelte --->
<script> <script>
import { myGlobalState } from './state.svelte.js'; import { setUserContext } from './context';
// ...
let { children } = $props();
setUserContext({ name: 'world' });
</script> </script>
{@render children()}
``` ```
This has a few drawbacks though: ```svelte
<!--- file: Child.svelte --->
<script>
import { getUserContext } from './context';
- it only safely works when your global state is only used client-side - for example, when you're building a single page application that does not render any of your components on the server. If your state ends up being managed and updated on the server, it could end up being shared between sessions and/or users, causing bugs const user = getUserContext();
- it may give the false impression that certain state is global when in reality it should only used in a certain part of your app </script>
To solve these drawbacks, Svelte provides a few `context` primitives which alleviate these problems. <h1>hello {user.name}, inside Child.svelte</h1>
```
```ts
/// file: context.ts
import { createContext } from 'svelte';
interface User {
name: string;
}
## Setting and getting context export const [getUserContext, setUserContext] = createContext<User>();
```
<!-- codeblock:end -->
> [!NOTE] `createContext` was added in version 5.40. If you are using an earlier version of Svelte, you must use `setContext` and `getContext` instead.
This is particularly useful when `Parent.svelte` is not directly aware of `Child.svelte`, but instead renders it as part of a `children` [snippet](snippet) as shown above.
To associate an arbitrary object with the current component, use `setContext`. ## `setContext` and `getContext`
As an alternative to `createContext`, you can use `setContext` and `getContext` directly. The parent component sets context with `setContext(key, value)`...
```svelte ```svelte
<!--- file: Parent.svelte --->
<script> <script>
import { setContext } from 'svelte'; import { setContext } from 'svelte';
setContext('key', value); setContext('my-context', 'hello from Parent.svelte');
</script> </script>
``` ```
The context is then available to children of the component (including slotted content) with `getContext`. ...and the child retrieves it with `getContext`:
```svelte ```svelte
<!--- file: Child.svelte --->
<script> <script>
import { getContext } from 'svelte'; import { getContext } from 'svelte';
const value = getContext('key'); const message = getContext('my-context');
</script> </script>
<h1>{message}, inside Child.svelte</h1>
``` ```
`setContext` and `getContext` solve the above problems: The key (`'my-context'`, in the example above) and the context itself can be any JavaScript value.
> [!NOTE] `createContext` is preferred since it provides better type safety and makes it unnecessary to use keys.
- the state is not global, it's scoped to the component. That way it's safe to render your components on the server and not leak state In addition to [`setContext`](svelte#setContext) and [`getContext`](svelte#getContext), Svelte exposes [`hasContext`](svelte#hasContext) and [`getAllContexts`](svelte#getAllContexts) functions.
- it's clear that the state is not global but rather scoped to a specific component tree and therefore can't be used in other parts of your app
> [!NOTE] `setContext`/`getContext` must be called during component initialisation. ## Using context with state
Context is not inherently reactive. If you need reactive values in context then you can pass a `$state` object into context, whose properties _will_ be reactive. You can store reactive state in context...
<!-- codeblock:start {"title":"Context with state"} -->
```svelte ```svelte
<!--- file: Parent.svelte ---> <!--- file: App.svelte --->
<script> <script>
import { setContext } from 'svelte'; import { setCounter } from './context.ts';
import Child from './Child.svelte';
let counter = $state({
count: 0
});
let value = $state({ count: 0 }); setCounter(counter);
setContext('counter', value);
</script> </script>
<button onclick={() => value.count++}>increment</button> <button onclick={() => counter.count += 1}>
increment
</button>
<Child />
<Child />
<Child />
<button onclick={() => counter.count = 0}>
reset
</button>
``` ```
```svelte ```svelte
<!--- file: Child.svelte ---> <!--- file: Child.svelte --->
<script> <script>
import { getContext } from 'svelte'; import { getCounter } from './context.ts';
const value = getContext('counter'); const counter = getCounter();
</script> </script>
<p>Count is {value.count}</p> <p>{counter.count}</p>
``` ```
To check whether a given `key` has been set in the context of a parent component, use `hasContext`. ```ts
/// file: context.ts
```svelte import { createContext } from 'svelte';
<script>
import { hasContext } from 'svelte';
if (hasContext('key')) { interface Counter {
// do something count: number;
} }
</script>
export const [getCounter, setCounter] = createContext<Counter>();
``` ```
<!-- codeblock:end -->
You can also retrieve the whole context map that belongs to the closest parent component using `getAllContexts`. This is useful, for example, if you programmatically create a component and want to pass the existing context to it. ...though note that if you _reassign_ `counter` instead of updating it, you will 'break the link' — in other words instead of this...
```svelte ```svelte
<script> <button onclick={() => counter = { count: 0 } }>
import { getAllContexts } from 'svelte'; reset
</button>
```
const contexts = getAllContexts(); ...you must do this:
</script>
```svelte
<button onclick={() => +++counter.count = 0+++}>
reset
</button>
``` ```
## Encapsulating context interactions Svelte will warn you if you get it wrong.
The above methods are very unopinionated about how to use them. When your app grows in scale, it's worthwhile to encapsulate setting and getting the context into functions and properly type them. Similarly, to pass primitive values through context, use functions as described in [Passing state into functions]($state#Passing-state-into-functions).
```ts ## Component testing
// @errors: 2304
import { getContext, setContext } from 'svelte'; When writing [component tests](testing#Unit-and-component-tests-with-Vitest-Component-testing), it can be useful to create a wrapper component that sets the context in order to check the behaviour of a component that uses it. As of version 5.49, you can do this sort of thing:
let userKey = Symbol('user'); ```js
import { mount, unmount } from 'svelte';
import { expect, test } from 'vitest';
import { setUserContext } from './context';
import MyComponent from './MyComponent.svelte';
export function setUserContext(user: User) { test('MyComponent', () => {
setContext(userKey, user); function Wrapper(...args) {
setUserContext({ name: 'Bob' });
return MyComponent(...args);
} }
export function getUserContext(): User { const component = mount(Wrapper, {
return getContext(userKey) as User; target: document.body
});
expect(document.body.innerHTML).toBe('<h1>Hello Bob!</h1>');
unmount(component);
});
```
This approach also works with [`hydrate`](imperative-component-api#hydrate) and [`render`](imperative-component-api#render).
## Replacing global state
When you have state shared by many different components, you might be tempted to put it in its own module and just import it wherever it's needed:
```js
/// file: state.svelte.js
export const myGlobalState = $state({
user: {
// ...
}
// ...
});
```
In many cases this is perfectly fine, but there is a risk: if you mutate the state during server-side rendering (which is discouraged, but entirely possible!)...
```svelte
<!--- file: App.svelte --->
<script>
import { myGlobalState } from './state.svelte.js';
let { data } = $props();
if (data.user) {
myGlobalState.user = data.user;
} }
</script>
``` ```
...then the data may be accessible by the _next_ user. Context solves this problem because it is not shared between requests.

@ -41,12 +41,10 @@ If a function is returned from `onMount`, it will be called when the component i
</script> </script>
``` ```
> [!NOTE] This behaviour will only work when the function passed to `onMount` _synchronously_ returns a value. `async` functions always return a `Promise`, and as such cannot _synchronously_ return a function. > [!NOTE] This behaviour will only work when the function passed to `onMount` is _synchronous_. `async` functions always return a `Promise`.
## `onDestroy` ## `onDestroy`
> EXPORT_SNIPPET: svelte#onDestroy
Schedules a callback to run immediately before the component is unmounted. Schedules a callback to run immediately before the component is unmounted.
Out of `onMount`, `beforeUpdate`, `afterUpdate` and `onDestroy`, this is the only one that runs inside a server-side component. Out of `onMount`, `beforeUpdate`, `afterUpdate` and `onDestroy`, this is the only one that runs inside a server-side component.
@ -96,7 +94,7 @@ Svelte 4 contained hooks that ran before and after the component as a whole was
</script> </script>
``` ```
Instead of `beforeUpdate` use `$effect.pre` and instead of `afterUpdate` use `$effect` instead - these runes offer more granular control and only react to the changes you're actually interested in. Instead of `beforeUpdate` use `$effect.pre` and instead of `afterUpdate` use `$effect` instead these runes offer more granular control and only react to the changes you're actually interested in.
### Chat window example ### Chat window example
@ -149,7 +147,7 @@ With runes, we can use `$effect.pre`, which behaves the same as `$effect` but ru
} }
function toggle() { function toggle() {
toggleValue = !toggleValue; theme = theme === 'dark' ? 'light' : 'dark';
} }
</script> </script>

@ -0,0 +1,123 @@
---
title: Hydratable data
---
In Svelte, when you want to render asynchronous content data on the server, you can simply `await` it. This is great! However, it comes with a pitfall: when hydrating that content on the client, Svelte has to redo the asynchronous work, which blocks hydration for however long it takes:
```svelte
<script>
import { getUser } from 'my-database-library';
// This will get the user on the server, render the user's name into the h1,
// and then, during hydration on the client, it will get the user _again_,
// blocking hydration until it's done.
const user = await getUser();
</script>
<h1>{user.name}</h1>
```
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions).
To fix the example above:
```svelte
<script>
import { hydratable } from 'svelte';
import { getUser } from 'my-database-library';
// During server rendering, this will serialize and stash the result of `getUser`, associating
// it with the provided key and baking it into the `head` content. During hydration, it will
// look for the serialized version, returning it instead of running `getUser`. After hydration
// is done, if it's called again, it'll simply invoke `getUser`.
const user = await hydratable('user', () => getUser());
</script>
<h1>{user.name}</h1>
```
This API can also be used to provide access to random or time-based values that are stable between server rendering and hydration. For example, to get a random number that doesn't update on hydration:
```ts
import { hydratable } from 'svelte';
const rand = hydratable('random', () => Math.random());
```
If you're a library author, be sure to prefix the keys of your `hydratable` values with the name of your library so that your keys don't conflict with other libraries.
## Serialization
All data returned from a `hydratable` function must be serializable. But this doesn't mean you're limited to JSON — Svelte uses [`devalue`](https://npmjs.com/package/devalue), which can serialize all sorts of things including `Map`, `Set`, `URL`, and `BigInt`. Check the documentation page for a full list. In addition to these, thanks to some Svelte magic, you can also fearlessly use promises:
```svelte
<script>
import { hydratable } from 'svelte';
const promises = hydratable('random', () => {
return {
one: Promise.resolve(1),
two: Promise.resolve(2)
}
});
</script>
{await promises.one}
{await promises.two}
```
## CSP
`hydratable` adds an inline `<script>` block to the `head` returned from `render`. If you're using [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) (CSP), this script will likely fail to run. You can provide a `nonce` to `render`:
```js
/// file: server.js
import { render } from 'svelte/server';
import App from './App.svelte';
// ---cut---
const nonce = crypto.randomUUID();
const { head, body } = await render(App, {
csp: { nonce }
});
```
This will add the `nonce` to the script block, on the assumption that you will later add the same nonce to the CSP header of the document that contains it:
```js
/// file: server.js
let response = new Response();
let nonce = 'xyz123';
// ---cut---
response.headers.set(
'Content-Security-Policy',
`script-src 'nonce-${nonce}'`
);
```
It's essential that a `nonce` — which, British slang definition aside, means 'number used once' — is only used when dynamically server rendering an individual response.
If instead you are generating static HTML ahead of time, you must use hashes instead:
```js
/// file: server.js
import { render } from 'svelte/server';
import App from './App.svelte';
// ---cut---
const { head, body, hashes } = await render(App, {
csp: { hash: true }
});
```
`hashes.script` will be an array of strings like `["sha256-abcd123"]`. As with `nonce`, the hashes should be used in your CSP header:
```js
/// file: server.js
let response = new Response();
let hashes = { script: ['sha256-xyz123'] };
// ---cut---
response.headers.set(
'Content-Security-Policy',
`script-src ${hashes.script.map((hash) => `'${hash}'`).join(' ')}`
);
```
We recommend using `nonce` over hash if you can, as `hash` will interfere with streaming SSR in the future.

@ -0,0 +1,7 @@
<!-- generated in ../../../../../packages/svelte/scripts/generate-browser-support.ts. do not edit -->
| Feature | Chrome/Edge | Firefox | Safari |
| - | - | - | - |
| [`$state.snapshot`](/docs/svelte/$state#$state.snapshot) | 98 | 94 | 15.4 |
| [`bind:devicePixelContentBoxSize`](/docs/svelte/bind#Dimensions) | <span style="color: var(--sk-fg-4)"></span> | 93 | not supported |
| [`flip` from `svelte/animate`](/docs/svelte/svelte-animate#flip) | <span style="color: var(--sk-fg-4)"></span> | 126 | <span style="color: var(--sk-fg-4)"></span> |

@ -0,0 +1,15 @@
<!-- generated in ../../../../../packages/svelte/scripts/generate-browser-support.ts. do not edit -->
| Browser | Minimum version |
| - | - |
| Chrome/Edge | 87 |
| Firefox | 83 |
| Safari | 14 |
| Opera | 73 |
| Opera (Android) | 62 |
| Samsung Internet | 14.0 |
| Android WebView | 87 |
| Internet Explorer | not supported |
> [!NOTE] This equates to a <a href="https://web-platform-dx.github.io/baseline/">Baseline</a> target of 2020.

@ -0,0 +1,185 @@
---
title: Best practices
skill: true
name: svelte-core-bestpractices
description: Guidance on writing fast, robust, modern Svelte code. Load this skill whenever in a Svelte project and asked to write/edit or analyze a Svelte component or module. Covers reactivity, event handling, styling, integration with libraries and more.
---
<!-- llm-ignore-start -->
This document outlines some best practices that will help you write fast, robust Svelte apps. It is also available as a `svelte-core-bestpractices` skill for your agents.
<!-- llm-ignore-end -->
## `$state`
Only use the `$state` rune for variables that should be _reactive_ — in other words, variables that cause an `$effect`, `$derived` or template expression to update. Everything else can be a normal variable.
Objects and arrays (`$state({...})` or `$state([...])`) are made deeply reactive, meaning mutation will trigger updates. This has a trade-off: in exchange for fine-grained reactivity, the objects must be proxied, which has performance overhead. In cases where you're dealing with large objects that are only ever reassigned (rather than mutated), use `$state.raw` instead. This is often the case with API responses, for example.
## `$derived`
To compute something from state, use `$derived` rather than `$effect`:
```js
// @errors: 2451
let num = 0;
// ---cut---
// do this
let square = $derived(num * num);
// don't do this
let square;
$effect(() => {
square = num * num;
});
```
> [!NOTE] `$derived` is given an expression, _not_ a function. If you need to use a function (because the expression is complex, for example) use `$derived.by`.
Deriveds are writable — you can assign to them, just like `$state`, except that they will re-evaluate when their expression changes.
If the derived expression is an object or array, it will be returned as-is — it is _not_ made deeply reactive. You can, however, use `$state` inside `$derived.by` in the rare cases that you need this.
## `$effect`
Effects are an escape hatch and should mostly be avoided. In particular, avoid updating state inside effects.
- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](@attach)
- If you need to run some code in response to user interaction, put the code directly in an event handler or use a [function binding](bind#Function-bindings) as appropriate
- If you need to log values for debugging purposes, use [`$inspect`]($inspect)
- If you need to observe something external to Svelte, use [`createSubscriber`](svelte-reactivity#createSubscriber)
Never wrap the contents of an effect in `if (browser) {...}` or similar — effects do not run on the server.
## `$props`
Treat props as though they will change. For example, values that depend on props should usually use `$derived`:
```js
// @errors: 2451
let { type } = $props();
// do this
let color = $derived(type === 'danger' ? 'red' : 'green');
// don't do this — `color` will not update if `type` changes
let color = type === 'danger' ? 'red' : 'green';
```
## `$inspect.trace`
`$inspect.trace` is a debugging tool for reactivity. If something is not updating properly or running more than it should you can add `$inspect.trace(label)` as the first line of an `$effect` or `$derived.by` (or any function they call) to trace their dependencies and discover which one triggered an update.
## Events
Any element attribute starting with `on` is treated as an event listener:
```svelte
<button onclick={() => {...}}>click me</button>
<!-- attribute shorthand also works -->
<button {onclick}>...</button>
<!-- so do spread attributes -->
<button {...props}>...</button>
```
If you need to attach listeners to `window` or `document` you can use `<svelte:window>` and `<svelte:document>`:
```svelte
<svelte:window onkeydown={...} />
<svelte:document onvisibilitychange={...} />
```
Avoid using `onMount` or `$effect` for this.
## Snippets
[Snippets](snippet) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](@render) tag, or passed to components as props. They must be declared within the template.
```svelte
{#snippet greeting(name)}
<p>hello {name}!</p>
{/snippet}
{@render greeting('world')}
```
> [!NOTE] Snippets declared at the top level of a component (i.e. not inside elements or blocks) can be referenced inside `<script>`. A snippet that doesn't reference component state is also available in a `<script module>`, in which case it can be exported for use by other components.
## Each blocks
Prefer to use [keyed each blocks](each#Keyed-each-blocks) — this improves performance by allowing Svelte to surgically insert or remove items rather than updating the DOM belonging to existing items.
> [!NOTE] The key _must_ uniquely identify the object. Do not use the index as a key.
Avoid destructuring if you need to mutate the item (with something like `bind:value={item.count}`, for example).
## Using JavaScript variables in CSS
If you have a JS variable that you want to use inside CSS you can set a CSS custom property with the `style:` directive.
```svelte
<div style:--columns={columns}>...</div>
```
You can then reference `var(--columns)` inside the component's `<style>`.
## Styling child components
The CSS in a component's `<style>` is scoped to that component. If a parent component needs to control the child's styles, the preferred way is to use CSS custom properties:
```svelte
<!-- Parent.svelte -->
<Child --color="red" />
<!-- Child.svelte -->
<h1>Hello</h1>
<style>
h1 {
color: var(--color);
}
</style>
```
If this is impossible (for example, the child component comes from a library) you can use `:global` to override styles:
```svelte
<div>
<Child />
</div>
<style>
div :global {
h1 {
color: red;
}
}
</style>
```
## Context
Consider using context instead of declaring state in a shared module. This will scope the state to the part of the app that needs it, and eliminate the possibility of it leaking between users when server-side rendering.
Use `createContext` rather than `setContext` and `getContext`, as it provides type safety.
## Async Svelte
If using version 5.36 or higher, you can use [await expressions](await-expressions) and [hydratable](hydratable) to use promises directly inside components. Note that these require the `experimental.async` option to be enabled in `svelte.config.js` as they are not yet considered fully stable.
## Avoid legacy features
Always use runes mode for new code, and avoid features that have more modern replacements:
- use `$state` instead of implicit reactivity (e.g. `let count = 0; count += 1`)
- use `$derived` and `$effect` instead of `$:` assignments and statements (but only use effects when there is no better solution)
- use `$props` instead of `export let`, `$$props` and `$$restProps`
- use `onclick={...}` instead of `on:click={...}`
- use `{#snippet ...}` and `{@render ...}` instead of `<slot>` and `$$slots` and `<svelte:fragment>`
- use `<DynamicComponent>` instead of `<svelte:component this={DynamicComponent}>`
- use `import Self from './ThisComponent.svelte'` and `<Self>` instead of `<svelte:self>`
- use classes with `$state` fields to share reactivity between components, instead of using stores
- use `{@attach ...}` instead of `use:action`
- use clsx-style arrays and objects in `class` attributes, instead of the `class:` directive

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save