mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Generally scripts should not be preloaded before images but if they arrive earlier than image preloads (or images) the network (or server) may be saturated responding to inflight script preloads and not sufficiently prioritize images arriving later. This change marks the preloaded bootstrap script with a `low` fetch priority to signal to supporting browsers that the request should be deprioritized. This should make the preload operate similar to async script fetch priority which is low by default according to https://web.dev/fetch-priority/ Additionally the bootstrap script preloads will emit before preinitialized scripts do. Normal script preloads will continue to be prioritized after stylesheets This change can land separatrely but is part of a larger effort to implement elevating image loading and making script loading less blocking. Later changes will emit used suspensey images earlier in the queue and will stop favoring scripts over images that are explicitly preloaded
199 lines
5.0 KiB
JavaScript
199 lines
5.0 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @emails react-core
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
let React;
|
|
let ReactDOMServer;
|
|
let Suspense;
|
|
|
|
describe('ReactDOMServerFB', () => {
|
|
beforeEach(() => {
|
|
jest.resetModules();
|
|
React = require('react');
|
|
ReactDOMServer = require('../ReactDOMServerFB');
|
|
Suspense = React.Suspense;
|
|
});
|
|
|
|
const theError = new Error('This is an error');
|
|
function Throw() {
|
|
throw theError;
|
|
}
|
|
const theInfinitePromise = new Promise(() => {});
|
|
function InfiniteSuspend() {
|
|
throw theInfinitePromise;
|
|
}
|
|
|
|
function readResult(stream) {
|
|
let result = '';
|
|
while (!ReactDOMServer.hasFinished(stream)) {
|
|
result += ReactDOMServer.renderNextChunk(stream);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
it('should be able to render basic HTML', async () => {
|
|
const stream = ReactDOMServer.renderToStream(<div>hello world</div>, {
|
|
onError(x) {
|
|
console.error(x);
|
|
},
|
|
});
|
|
const result = readResult(stream);
|
|
expect(result).toMatchInlineSnapshot(`"<div>hello world</div>"`);
|
|
});
|
|
|
|
it('should emit bootstrap script src at the end', () => {
|
|
const stream = ReactDOMServer.renderToStream(<div>hello world</div>, {
|
|
bootstrapScriptContent: 'INIT();',
|
|
bootstrapScripts: ['init.js'],
|
|
bootstrapModules: ['init.mjs'],
|
|
onError(x) {
|
|
console.error(x);
|
|
},
|
|
});
|
|
const result = readResult(stream);
|
|
expect(result).toMatchInlineSnapshot(
|
|
`"<link rel="preload" href="init.js" as="script" fetchPriority="low"/><link rel="modulepreload" href="init.mjs" fetchPriority="low"/><div>hello world</div><script>INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
|
|
);
|
|
});
|
|
|
|
it('emits all HTML as one unit if we wait until the end to start', async () => {
|
|
let hasLoaded = false;
|
|
let resolve;
|
|
const promise = new Promise(r => (resolve = r));
|
|
function Wait() {
|
|
if (!hasLoaded) {
|
|
throw promise;
|
|
}
|
|
return 'Done';
|
|
}
|
|
const stream = ReactDOMServer.renderToStream(
|
|
<div>
|
|
<Suspense fallback="Loading">
|
|
<Wait />
|
|
</Suspense>
|
|
</div>,
|
|
{
|
|
onError(x) {
|
|
console.error(x);
|
|
},
|
|
},
|
|
);
|
|
await jest.runAllTimers();
|
|
// Resolve the loading.
|
|
hasLoaded = true;
|
|
await resolve();
|
|
|
|
await jest.runAllTimers();
|
|
|
|
const result = readResult(stream);
|
|
expect(result).toMatchInlineSnapshot(`"<div><!--$-->Done<!--/$--></div>"`);
|
|
});
|
|
|
|
it('should throw an error when an error is thrown at the root', () => {
|
|
const reportedErrors = [];
|
|
const stream = ReactDOMServer.renderToStream(
|
|
<div>
|
|
<Throw />
|
|
</div>,
|
|
{
|
|
onError(x) {
|
|
reportedErrors.push(x);
|
|
},
|
|
},
|
|
);
|
|
|
|
let caughtError = null;
|
|
let result = '';
|
|
try {
|
|
result = readResult(stream);
|
|
} catch (x) {
|
|
caughtError = x;
|
|
}
|
|
expect(caughtError).toBe(theError);
|
|
expect(result).toBe('');
|
|
expect(reportedErrors).toEqual([theError]);
|
|
});
|
|
|
|
it('should throw an error when an error is thrown inside a fallback', () => {
|
|
const reportedErrors = [];
|
|
const stream = ReactDOMServer.renderToStream(
|
|
<div>
|
|
<Suspense fallback={<Throw />}>
|
|
<InfiniteSuspend />
|
|
</Suspense>
|
|
</div>,
|
|
{
|
|
onError(x) {
|
|
reportedErrors.push(x);
|
|
},
|
|
},
|
|
);
|
|
|
|
let caughtError = null;
|
|
let result = '';
|
|
try {
|
|
result = readResult(stream);
|
|
} catch (x) {
|
|
caughtError = x;
|
|
}
|
|
expect(caughtError).toBe(theError);
|
|
expect(result).toBe('');
|
|
expect(reportedErrors).toEqual([theError]);
|
|
});
|
|
|
|
it('should not throw an error when an error is thrown inside suspense boundary', async () => {
|
|
const reportedErrors = [];
|
|
const stream = ReactDOMServer.renderToStream(
|
|
<div>
|
|
<Suspense fallback={<div>Loading</div>}>
|
|
<Throw />
|
|
</Suspense>
|
|
</div>,
|
|
{
|
|
onError(x) {
|
|
reportedErrors.push(x);
|
|
},
|
|
},
|
|
);
|
|
|
|
const result = readResult(stream);
|
|
expect(result).toContain('Loading');
|
|
expect(reportedErrors).toEqual([theError]);
|
|
});
|
|
|
|
it('should be able to complete by aborting even if the promise never resolves', () => {
|
|
const errors = [];
|
|
const stream = ReactDOMServer.renderToStream(
|
|
<div>
|
|
<Suspense fallback={<div>Loading</div>}>
|
|
<InfiniteSuspend />
|
|
</Suspense>
|
|
</div>,
|
|
{
|
|
onError(x) {
|
|
errors.push(x.message);
|
|
},
|
|
},
|
|
);
|
|
|
|
const partial = ReactDOMServer.renderNextChunk(stream);
|
|
expect(partial).toContain('Loading');
|
|
|
|
ReactDOMServer.abortStream(stream);
|
|
|
|
const remaining = readResult(stream);
|
|
expect(remaining).toEqual('');
|
|
|
|
expect(errors).toEqual([
|
|
'The render was aborted by the server without a reason.',
|
|
]);
|
|
});
|
|
});
|