mirror of https://github.com/sveltejs/svelte
commit
0b2fdaf492
@ -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: unlink errored and otherwise finished batch
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
perf: walk composedPath() directly in delegated event propagation
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: transfer effects when merging batches
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: remove temporary raw-text hydration markers
|
||||
@ -1,5 +0,0 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: don't swallow `DOMException` when `media.play()` fails in `bind:paused`
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: declare `let:` directives before `{@const}` declarations on slotted elements
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: correctly coordinate component-level effects inside async blocks
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: make unnecessary commit work less likely
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
chore: add tag name to `a11y_click_events_have_key_events` warning
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: catch rejected promises while merging/committing
|
||||
@ -1,5 +0,0 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: reduce if block nesting
|
||||
@ -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
|
||||
@ -0,0 +1,2 @@
|
||||
https://svelte.dev/funding.json
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
# 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.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
If asked to do a performance investigation, use the `performance-investigation` skill.
|
||||
@ -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,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}`
|
||||
);
|
||||
}
|
||||
@ -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