feat: skip pending block for already-resolved promises (#12274)

* feat: skip pending block for already-resolved promises

* update tests

* update docs
pull/12456/head
Rich Harris 2 months ago committed by GitHub
parent b1cf2ece63
commit c92620dcbe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: skip pending block for already-resolved promises

@ -139,7 +139,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
} else {
// Wait a microtask before checking if we should show the pending state as
// the promise might have resolved by the next microtask.
queue_micro_task(() => {
Promise.resolve().then(() => {
if (!resolved) update(PENDING, true);
});
}

@ -1,4 +1,4 @@
<div class="a svelte-xyz"></div>
<div class="d svelte-xyz"></div>
<div class="f svelte-xyz"></div>
<div class="h svelte-xyz"></div>
<div class="b svelte-xyz"></div>
<div class="g svelte-xyz"></div>
<div class="h svelte-xyz"></div>

@ -1,7 +1,3 @@
<script>
let promise = Promise.resolve();
</script>
<style>
.a ~ .b { color: green; }
.a ~ .c { color: green; }
@ -20,19 +16,20 @@
<div class="a"></div>
{#await promise then value}
<!-- non-promise, so that something renders initially -->
{#await true then value}
<div class="b"></div>
{:catch error}
<div class="c"></div>
{/await}
{#await promise}
{#await true}
<div class="d"></div>
{:catch error}
<div class="e"></div>
{/await}
{#await promise}
{#await true}
<div class="f"></div>
{:then error}
<div class="g"></div>

@ -5,20 +5,20 @@ export default test({
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".b ~ .c"',
start: { character: 269, column: 1, line: 15 },
end: { character: 276, column: 8, line: 15 }
start: { character: 217, column: 1, line: 13 },
end: { character: 224, column: 8, line: 13 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".c ~ .d"',
start: { character: 296, column: 1, line: 16 },
end: { character: 303, column: 8, line: 16 }
start: { character: 242, column: 1, line: 14 },
end: { character: 249, column: 8, line: 14 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".b ~ .d"',
start: { character: 323, column: 1, line: 17 },
end: { character: 330, column: 8, line: 17 }
start: { character: 267, column: 1, line: 15 },
end: { character: 274, column: 8, line: 15 }
}
]
});

@ -7,6 +7,6 @@
.a.svelte-xyz ~ .e:where(.svelte-xyz) { color: green; }
/* no match */
/* (unused) .b ~ .c { color: green; }*/
/* (unused) .c ~ .d { color: green; }*/
/* (unused) .b ~ .d { color: green; }*/
/* (unused) .b ~ .c { color: red; }*/
/* (unused) .c ~ .d { color: red; }*/
/* (unused) .b ~ .d { color: red; }*/

@ -1,3 +1,3 @@
<div class="a svelte-xyz"></div>
<div class="b svelte-xyz"></div>
<div class="e svelte-xyz"></div>
<div class="c svelte-xyz"></div>
<div class="e svelte-xyz"></div>

@ -1,6 +1,4 @@
<script>
let promise = Promise.resolve();
</script>
<style>
.a ~ .b { color: green; }
@ -12,14 +10,15 @@
.a ~ .e { color: green; }
/* no match */
.b ~ .c { color: green; }
.c ~ .d { color: green; }
.b ~ .d { color: green; }
.b ~ .c { color: red; }
.c ~ .d { color: red; }
.b ~ .d { color: red; }
</style>
<div class="a"></div>
{#await promise}
<!-- non-promise, so that something renders initially -->
{#await true}
<div class="b"></div>
{:then value}
<div class="c"></div>

@ -1,4 +1,4 @@
<div class="a svelte-xyz"></div>
<div class="d svelte-xyz"></div>
<div class="f svelte-xyz"></div>
<div class="h svelte-xyz"></div>
<div class="b svelte-xyz"></div>
<div class="g svelte-xyz"></div>
<div class="h svelte-xyz"></div>

@ -1,7 +1,3 @@
<script>
let promise = Promise.resolve();
</script>
<style>
.a + .b { color: green; }
.a + .c { color: green; }
@ -20,21 +16,22 @@
<div class="a"></div>
{#await promise then value}
<!-- non-promise, so that something renders initially -->
{#await true then value}
<div class="b"></div>
{:catch error}
<div class="c"></div>
{/await}
{#await promise}
{#await true}
<div class="d"></div>
{:catch error}
<div class="e"></div>
{/await}
{#await promise}
{#await true}
<div class="f"></div>
{:then error}
{:then value}
<div class="g"></div>
{/await}

@ -5,26 +5,26 @@ export default test({
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".a + .e"',
start: { character: 242, column: 1, line: 14 },
end: { character: 249, column: 8, line: 14 }
start: { character: 188, column: 1, line: 10 },
end: { character: 195, column: 8, line: 10 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".b + .c"',
start: { character: 269, column: 1, line: 15 },
end: { character: 276, column: 8, line: 15 }
start: { character: 213, column: 1, line: 11 },
end: { character: 220, column: 8, line: 11 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".c + .d"',
start: { character: 296, column: 1, line: 16 },
end: { character: 303, column: 8, line: 16 }
start: { character: 238, column: 1, line: 12 },
end: { character: 245, column: 8, line: 12 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".b + .d"',
start: { character: 323, column: 1, line: 17 },
end: { character: 330, column: 8, line: 17 }
start: { character: 263, column: 1, line: 13 },
end: { character: 270, column: 8, line: 13 }
}
]
});

@ -6,7 +6,7 @@
.d.svelte-xyz + .e:where(.svelte-xyz) { color: green; }
/* no match */
/* (unused) .a + .e { color: green; }*/
/* (unused) .b + .c { color: green; }*/
/* (unused) .c + .d { color: green; }*/
/* (unused) .b + .d { color: green; }*/
/* (unused) .a + .e { color: red; }*/
/* (unused) .b + .c { color: red; }*/
/* (unused) .c + .d { color: red; }*/
/* (unused) .b + .d { color: red; }*/

@ -1,3 +1,3 @@
<div class="a svelte-xyz"></div>
<div class="b svelte-xyz"></div>
<div class="e svelte-xyz"></div>
<div class="c svelte-xyz"></div>
<div class="e svelte-xyz"></div>

@ -1,7 +1,3 @@
<script>
let promise = Promise.resolve();
</script>
<style>
.a + .b { color: green; }
.a + .c { color: green; }
@ -11,15 +7,16 @@
.d + .e { color: green; }
/* no match */
.a + .e { color: green; }
.b + .c { color: green; }
.c + .d { color: green; }
.b + .d { color: green; }
.a + .e { color: red; }
.b + .c { color: red; }
.c + .d { color: red; }
.b + .d { color: red; }
</style>
<div class="a"></div>
{#await promise}
<!-- non-promise, so that something renders initially -->
{#await true}
<div class="b"></div>
{:then value}
<div class="c"></div>

@ -7,10 +7,6 @@ export default test({
};
},
html: `
Waiting...
`,
async test({ assert, component, target }) {
await (component.thePromise = Promise.resolve({ func: 12345 }));

@ -13,11 +13,6 @@ export default test({
return { thePromise: deferred.promise };
},
html: `
<br />
<p>the promise is pending</p>
`,
async test({ assert, component, target }) {
deferred.resolve(42);
@ -27,6 +22,7 @@ export default test({
deferred = create_deferred();
component.thePromise = deferred.promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '<br /><p>the promise is pending</p>');

@ -2,10 +2,6 @@ import { test } from '../../test';
import { sleep } from './sleep.js';
export default test({
html: `
<p>loading...</p>
`,
test({ assert, target }) {
return sleep(50).then(() => {
assert.htmlEqual(

@ -13,10 +13,6 @@ export default test({
return { thePromise: deferred.promise, show: true };
},
html: `
<div><p>loading...</p></div>
`,
test({ assert, component, target }) {
deferred.resolve(42);

@ -19,10 +19,6 @@ export default test({
return { items };
},
html: `
<p>a title: loading...</p>
`,
test({ assert, target }) {
fulfil(42);

@ -1,7 +1,6 @@
import { test } from '../../test';
export default test({
html: 'Loading...',
async test({ assert, component, target }) {
await component.test();

@ -1,9 +1,8 @@
import { test } from '../../test';
export default test({
html: '<p>wait for it...</p>',
test({ assert, component, target }) {
return component.promise.then(async () => {
return component.promise.then(() => {
assert.htmlEqual(
target.innerHTML,
`

@ -7,6 +7,7 @@ export default test({
});
component.promise = promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '<p>wait for it...</p>');

@ -3,8 +3,6 @@ import { ok, test } from '../../test';
export default test({
async test({ assert, component, target }) {
assert.htmlEqual(target.innerHTML, 'Loading...');
await component.promise;
await Promise.resolve();
const span = target.querySelector('span');

@ -13,15 +13,11 @@ export default test({
return { thePromise: deferred.promise };
},
html: `
<div><p>loading...</p></div>
`,
test({ assert, component, target }) {
deferred.resolve(42);
return deferred.promise
.then(() => {
.then(async () => {
assert.htmlEqual(
target.innerHTML,
`
@ -32,6 +28,7 @@ export default test({
deferred = create_deferred();
component.thePromise = deferred.promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '<div><p>loading...</p></div>');

@ -15,10 +15,6 @@ export default test({
};
},
html: `
<p>loading...</p>
`,
test({ assert, component, target, window }) {
deferred.resolve(42);

@ -12,10 +12,6 @@ export default test({
return { show: true, thePromise };
},
html: `
<p>loading...</p>
`,
test({ assert, component, target }) {
fulfil(42);

@ -13,10 +13,6 @@ export default test({
return { thePromise: deferred.promise };
},
html: `
<p>loading...</p>
`,
async test({ assert, component, target }) {
deferred.resolve(42);
@ -25,6 +21,7 @@ export default test({
deferred = create_deferred();
component.thePromise = deferred.promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '<p>loading...</p>');
deferred.reject(new Error('something broke'));

@ -13,16 +13,11 @@ export default test({
return { thePromise: deferred.promise };
},
html: `
<p>loading...</p>
<p>loading...</p>
`,
test({ assert, component, target }) {
deferred.resolve(42);
return deferred.promise
.then(() => {
.then(async () => {
assert.htmlEqual(
target.innerHTML,
`
@ -34,6 +29,7 @@ export default test({
deferred = create_deferred();
component.thePromise = deferred.promise;
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,

@ -13,18 +13,17 @@ export default test({
return { thePromise: deferred.promise };
},
html: 'waiting',
test({ assert, component, target }) {
deferred.resolve(9000);
return deferred.promise
.then(() => {
.then(async () => {
assert.htmlEqual(target.innerHTML, 'resolved');
deferred = create_deferred();
component.thePromise = deferred.promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, 'waiting');

@ -12,10 +12,6 @@ export default test({
return { thePromise };
},
html: `
<p>loading...</p><p>true!</p>
`,
test({ assert, target }) {
fulfil(42);

@ -12,15 +12,11 @@ export default test({
return { promise };
},
html: `
<p>loading...</p>
`,
test({ assert, component, target }) {
fulfil(42);
return promise
.then(() => {
.then(async () => {
assert.htmlEqual(
target.innerHTML,
`
@ -33,6 +29,7 @@ export default test({
});
component.promise = promise;
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,

@ -13,15 +13,11 @@ export default test({
return { thePromise: deferred.promise };
},
html: `
<p>loading...</p>
`,
test({ assert, component, target }) {
deferred.resolve(42);
return deferred.promise
.then(() => {
.then(async () => {
assert.htmlEqual(
target.innerHTML,
`
@ -32,6 +28,7 @@ export default test({
deferred = create_deferred();
component.thePromise = deferred.promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '<p>loading...</p>');

@ -7,10 +7,6 @@ export default test({
};
},
html: `
loading...
`,
async test({ assert, component, target }) {
await (component.thePromise = Promise.resolve([1, 2, 3, 4, 5, 6, 7, 8]));

@ -7,10 +7,6 @@ export default test({
};
},
html: `
loading...
`,
async test({ assert, component, target }) {
await (component.thePromise = Promise.resolve([1, 2]));

@ -7,10 +7,6 @@ export default test({
};
},
html: `
loading...
`,
async test({ assert, component, target }) {
await (component.thePromise = Promise.resolve([10, 11, 12, 13, 14, 15]));

@ -7,10 +7,6 @@ export default test({
};
},
html: `
loading...
`,
async test({ assert, component, target }) {
await (component.thePromise = Promise.resolve({ error: 'error message' }));
assert.htmlEqual(

@ -12,10 +12,6 @@ export default test({
return { thePromise };
},
html: `
loading...
`,
async test({ assert, target }) {
fulfil([]);

@ -13,12 +13,6 @@ export default test({
return { thePromise: deferred.promise };
},
html: `
<br>
<br>
<p>the promise is pending</p>
`,
expect_unhandled_rejections: true,
async test({ assert, component, target }) {
deferred.resolve();
@ -39,6 +33,7 @@ export default test({
const local = (deferred = create_deferred());
component.thePromise = local.promise;
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,

@ -19,7 +19,7 @@ export default test({
deferred.resolve(42);
return deferred.promise
.then(() => {
.then(async () => {
assert.htmlEqual(
target.innerHTML,
`
@ -30,6 +30,7 @@ export default test({
deferred = create_deferred();
component.thePromise = deferred.promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '');

@ -11,6 +11,7 @@ export default test({
let promise = new Promise((ok) => (resolve = ok));
component.promise = promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, 'Loading...');
resolve(42);
@ -19,6 +20,7 @@ export default test({
promise = new Promise((ok, fail) => (reject = fail));
component.promise = promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, 'Loading...');
reject(99);
@ -27,6 +29,7 @@ export default test({
promise = new Promise((ok) => (resolve = ok));
component.promise = promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, 'Loading...');
resolve(1);

@ -8,10 +8,6 @@ export default test({
};
},
html: `
<div><p>loading...</p></div>
`,
async test({ assert, component, target }) {
await (component.thePromise = Promise.resolve({
value: 'success',

@ -8,10 +8,6 @@ export default test({
};
},
html: `
<div><p>loading...</p></div>
`,
async test({ assert, component, target }) {
await (component.thePromise = Promise.resolve(component.Component));

@ -13,21 +13,18 @@ export default test({
return { promise: deferred.promise };
},
html: `
<p>loading...</p>
`,
expect_unhandled_rejections: true,
test({ assert, component, target }) {
deferred.resolve(42);
return deferred.promise
.then(() => {
.then(async () => {
assert.htmlEqual(target.innerHTML, '<p>loaded</p>');
deferred = create_deferred();
component.promise = deferred.promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '<p>loading...</p>');

@ -1,10 +1,6 @@
import { test } from '../../test';
export default test({
html: `
<p>...waiting</p>
`,
async test({ assert, component, target }) {
await component.promise;

@ -14,6 +14,7 @@ export default test({
intro: true,
async test({ assert, target, component, raf }) {
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '<p class="pending" foo="0.0">loading...</p>');
let time = 0;

@ -16,7 +16,8 @@ export default test({
intro: true,
test({ assert, target, raf }) {
async test({ assert, target, raf }) {
await Promise.resolve();
const p = /** @type {HTMLParagraphElement & { foo: number }} */ (target.querySelector('p'));
raf.tick(0);

@ -1,7 +1,7 @@
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
async test({ assert, target, logs, variant }) {
const [b1, b2, b3, b4] = target.querySelectorAll('button');
b1.click();
await Promise.resolve();
@ -16,6 +16,9 @@ export default test({
b4.click();
await Promise.resolve();
await Promise.resolve();
assert.deepEqual(logs, ['pending', 'a', 'b', 'c', 'pending']);
assert.deepEqual(
logs,
variant === 'hydrate' ? ['pending', 'a', 'b', 'c', 'pending'] : ['a', 'b', 'c', 'pending']
);
}
});

@ -1,7 +1,7 @@
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
async test({ assert, target, logs, variant }) {
const [b1, b2] = target.querySelectorAll('button');
b1.click();
await Promise.resolve();
@ -22,6 +22,11 @@ export default test({
`<p>then b</p><button>Show Promise A</button><button>Show Promise B</button>`
);
assert.deepEqual(logs, ['rendering pending block', 'rendering then block']);
assert.deepEqual(
logs,
variant === 'hydrate'
? ['rendering pending block', 'rendering then block']
: ['rendering then block']
);
}
});

@ -318,3 +318,24 @@ Since these mismatches are extremely rare, Svelte 5 assumes that the values are
{@html markup}
<img {src} />
```
### `await` blocks delay render
In Svelte 4, an `{#await ...}` block immediately renders the pending section. In some cases, this is wasteful, because the promise is already resolved.
In Svelte 5 the block remains unrendered when mounting or updating the promise, until we know whether it is already resolved or not — if so, we initally render then `{:then ...}` or `{:catch ...}` section instead.
This does _not_ apply during hydration, since the pending section was already server-rendered.
To wait until the pending section has been rendered (for example during testing), use `await Promise.resolve()` after mounting or updating the promise:
```diff
let props = {
promise: getPromiseSomehow()
};
mount(App, { target, props });
+await Promise.resolve();
assert.equal(target.innerHTML, '...');
```

Loading…
Cancel
Save