mirror of https://github.com/sveltejs/svelte
commit
3321433bc1
@ -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
|
||||
@ -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
|
||||
@ -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 }}
|
||||
}
|
||||
@ -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,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"
|
||||
}
|
||||
@ -1,6 +1,3 @@
|
||||
{
|
||||
"search.exclude": {
|
||||
"sites/svelte-5-preview/static/*": true
|
||||
},
|
||||
"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.
|
||||
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 102 KiB |
@ -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++;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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 = [];
|
||||
for (const benchmark of benchmarks) {
|
||||
const result = await benchmark();
|
||||
console.error(result.benchmark);
|
||||
results.push(result);
|
||||
const PROFILE_DIR = process.env.BENCH_PROFILE_DIR;
|
||||
|
||||
for (let i = 0; i < reactivity_benchmarks.length; i += 1) {
|
||||
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);
|
||||
|
||||
@ -1,55 +1,94 @@
|
||||
import * as $ from '../packages/svelte/src/internal/client/index.js';
|
||||
import { reactivity_benchmarks } from './benchmarks/reactivity/index.js';
|
||||
import { ssr_benchmarks } from './benchmarks/ssr/index.js';
|
||||
import { with_cpu_profile } from './utils.js';
|
||||
|
||||
let total_time = 0;
|
||||
let total_gc_time = 0;
|
||||
// e.g. `pnpm bench kairo` to only run the kairo benchmarks
|
||||
const filters = process.argv.slice(2);
|
||||
|
||||
const PROFILE_DIR = './benchmarking/.profiles';
|
||||
|
||||
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);
|
||||
|
||||
try {
|
||||
for (const { benchmarks, name } of suites) {
|
||||
let suite_time = 0;
|
||||
let suite_gc_time = 0;
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
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) {
|
||||
const results = await benchmark();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(results);
|
||||
total_time += Number(results.time);
|
||||
total_gc_time += Number(results.gc_time);
|
||||
suite_time += Number(results.time);
|
||||
suite_gc_time += Number(results.gc_time);
|
||||
const results = await with_cpu_profile(PROFILE_DIR, benchmark.label, () => benchmark.fn());
|
||||
console.log(
|
||||
pad_right(benchmark.label, COLUMN_WIDTHS[0]) +
|
||||
pad_left(results.time.toFixed(2), COLUMN_WIDTHS[1]) +
|
||||
pad_left(results.gc_time.toFixed(2), COLUMN_WIDTHS[2])
|
||||
);
|
||||
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
|
||||
console.log({
|
||||
suite_time: suite_time.toFixed(2),
|
||||
suite_gc_time: suite_gc_time.toFixed(2)
|
||||
});
|
||||
if (PROFILE_DIR !== null) {
|
||||
console.log(`\nCPU profiles written to ${PROFILE_DIR}`);
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\x1b[1m', '\n-- Benchmarking Failed --\n', '\x1b[0m');
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
$.pop();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\x1b[1m', '\n-- Benchmarking Complete --\n', '\x1b[0m');
|
||||
// eslint-disable-next-line no-console
|
||||
console.log({
|
||||
total_time: total_time.toFixed(2),
|
||||
total_gc_time: total_gc_time.toFixed(2)
|
||||
});
|
||||
|
||||
console.log('');
|
||||
|
||||
console.log(
|
||||
pad_right('total', COLUMN_WIDTHS[0]) +
|
||||
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 fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import inspector from 'node:inspector/promises';
|
||||
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.
|
||||
|
||||
class GarbageTrack {
|
||||
track_id = 0;
|
||||
observer = new PerformanceObserver((list) => this.perf_entries.push(...list.getEntries()));
|
||||
perf_entries = [];
|
||||
periods = [];
|
||||
async function track(fn) {
|
||||
v8.collectGarbage();
|
||||
|
||||
/** @type {PerformanceEntry[]} */
|
||||
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 result = fn();
|
||||
fn();
|
||||
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) {
|
||||
await promise_delay(10);
|
||||
export async function fastest_test(times, fn) {
|
||||
/** @type {Array<{ time: number, gc_time: number }>} */
|
||||
const results = [];
|
||||
|
||||
const period = this.periods.find((period) => period.track_id === track_id);
|
||||
if (!period) {
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
|
||||
return Promise.reject('no period found');
|
||||
for (let i = 0; i < times; i++) {
|
||||
results.push(await track(fn));
|
||||
}
|
||||
|
||||
const entries = this.perf_entries.filter(
|
||||
(e) => e.startTime >= period.start && e.startTime < period.end
|
||||
);
|
||||
return entries.reduce((t, e) => e.duration + t, 0);
|
||||
return results.reduce((a, b) => (a.time < b.time ? a : b));
|
||||
}
|
||||
|
||||
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) {
|
||||
return new Promise((resolve) => setTimeout(resolve, timeout));
|
||||
if (path.isAbsolute(value)) {
|
||||
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) {
|
||||
const start = performance.now();
|
||||
const result = fn();
|
||||
const time = performance.now() - start;
|
||||
return { result, time };
|
||||
function is_special_runtime_node(function_name) {
|
||||
return function_name === '(idle)' || function_name === '(garbage collector)';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {() => void} fn
|
||||
* @param {string} normalized_url
|
||||
*/
|
||||
async function run_tracked(fn) {
|
||||
v8.collectGarbage();
|
||||
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 } };
|
||||
function is_svelte_source_url(normalized_url) {
|
||||
return normalized_url.startsWith('packages/svelte/');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} times
|
||||
* @param {() => void} fn
|
||||
* @param {Record<string, unknown>} profile
|
||||
*/
|
||||
export async function fastest_test(times, fn) {
|
||||
const results = [];
|
||||
for (let i = 0; i < times; i++) {
|
||||
const run = await run_tracked(fn);
|
||||
results.push(run);
|
||||
function profile_to_markdown(profile) {
|
||||
/** @type {string[]} */
|
||||
const lines = ['# CPU profile'];
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} a
|
||||
*/
|
||||
export function assert(a) {
|
||||
if (!a) {
|
||||
throw new Error('Assertion failed');
|
||||
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 : '(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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} file
|
||||
*/
|
||||
export function read_file(file) {
|
||||
return fs.readFileSync(file, 'utf-8').replace(/\r\n/g, '\n');
|
||||
/** @type {Map<number, number>} */
|
||||
const self_sample_count = new Map();
|
||||
for (const sample of samples) {
|
||||
if (typeof sample !== 'number') continue;
|
||||
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
|
||||
* @param {string} contents
|
||||
* @template T
|
||||
* @param {string | null} profile_dir
|
||||
* @param {string} profile_name
|
||||
* @param {() => T | Promise<T>} fn
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
export function write(file, contents) {
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
} catch {}
|
||||
export async function with_cpu_profile(profile_dir, profile_name, fn) {
|
||||
if (profile_dir === null) {
|
||||
return await fn();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -1,5 +1,6 @@
|
||||
---
|
||||
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.
|
||||
@ -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>
|
||||
```
|
||||
@ -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).
|
||||
@ -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…
Reference in new issue