Skip to content

Commit 804447c

Browse files
authored
fix(runtime): use link to preload js assets (#2071)
1 parent 36f9f4e commit 804447c

File tree

6 files changed

+168
-51
lines changed

6 files changed

+168
-51
lines changed

.changeset/forty-dolls-dance.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@module-federation/runtime': patch
3+
'@module-federation/sdk': patch
4+
---
5+
6+
fix(runtime): use link to preload js

packages/runtime/__tests__/__snapshots__/preload-remote.spec.ts.snap

Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,19 @@
33
exports[`preload-remote inBrowser > 1 preload with default config 1`] = `
44
{
55
"links": [
6+
{
7+
"href": "http://localhost:1111/resources/preload/preload-resource/button.sync.js",
8+
"rel": "preload",
9+
"type": "script",
10+
},
11+
{
12+
"href": "http://localhost:1111/resources/preload/preload-resource/sub1-button/button.sync.js",
13+
"rel": "preload",
14+
"type": "script",
15+
},
616
{
717
"href": "http://localhost:1111/resources/preload/preload-resource/button.sync.css",
18+
"rel": "preload",
819
"type": "style",
920
},
1021
],
@@ -17,14 +28,6 @@ exports[`preload-remote inBrowser > 1 preload with default config 1`] = `
1728
"crossorigin": "",
1829
"src": "http://localhost:1111/resources/preload/preload-resource/sub1-button/federation-remote-entry.js",
1930
},
20-
{
21-
"crossorigin": "",
22-
"src": "http://localhost:1111/resources/preload/preload-resource/button.sync.js",
23-
},
24-
{
25-
"crossorigin": "",
26-
"src": "http://localhost:1111/resources/preload/preload-resource/sub1-button/button.sync.js",
27-
},
2831
],
2932
}
3033
`;
@@ -33,78 +36,90 @@ exports[`preload-remote inBrowser > 2 preload with all config 1`] = `
3336
{
3437
"links": [
3538
{
36-
"href": "http://localhost:1111/resources/preload/preload-resource/sub2/button.async.css",
37-
"type": "style",
39+
"href": "http://localhost:1111/resources/preload/preload-resource/sub2/button.async.js",
40+
"rel": "preload",
41+
"type": "script",
3842
},
3943
{
40-
"href": "http://localhost:1111/resources/preload/preload-resource/sub2/button.sync.css",
41-
"type": "style",
44+
"href": "http://localhost:1111/resources/preload/preload-resource/sub2/button.sync.js",
45+
"rel": "preload",
46+
"type": "script",
4247
},
43-
],
44-
"scripts": [
4548
{
46-
"crossorigin": "",
47-
"src": "http://localhost:1111/resources/preload/preload-resource/sub2/federation-remote-entry.js",
49+
"href": "http://localhost:1111/resources/preload/preload-resource/sub2-button/button.async.js",
50+
"rel": "preload",
51+
"type": "script",
4852
},
4953
{
50-
"crossorigin": "",
51-
"src": "http://localhost:1111/resources/preload/preload-resource/sub2-button/federation-remote-entry.js",
54+
"href": "http://localhost:1111/resources/preload/preload-resource/sub2-button/button.sync.js",
55+
"rel": "preload",
56+
"type": "script",
5257
},
5358
{
54-
"crossorigin": "",
55-
"src": "http://localhost:1111/resources/preload/preload-resource/sub2/button.async.js",
59+
"href": "http://localhost:1111/resources/preload/preload-resource/sub2-add/add.async.js",
60+
"rel": "preload",
61+
"type": "script",
5662
},
5763
{
58-
"crossorigin": "",
59-
"src": "http://localhost:1111/resources/preload/preload-resource/sub2/button.sync.js",
64+
"href": "http://localhost:1111/resources/preload/preload-resource/sub2-add/add.sync.js",
65+
"rel": "preload",
66+
"type": "script",
6067
},
6168
{
62-
"crossorigin": "",
63-
"src": "http://localhost:1111/resources/preload/preload-resource/sub2-button/button.async.js",
69+
"href": "http://localhost:1111/resources/preload/preload-resource/sub2/button.async.css",
70+
"rel": "preload",
71+
"type": "style",
6472
},
6573
{
66-
"crossorigin": "",
67-
"src": "http://localhost:1111/resources/preload/preload-resource/sub2-button/button.sync.js",
74+
"href": "http://localhost:1111/resources/preload/preload-resource/sub2/button.sync.css",
75+
"rel": "preload",
76+
"type": "style",
6877
},
78+
],
79+
"scripts": [
6980
{
7081
"crossorigin": "",
71-
"src": "http://localhost:1111/resources/preload/preload-resource/sub2-add/add.async.js",
82+
"src": "http://localhost:1111/resources/preload/preload-resource/sub2/federation-remote-entry.js",
7283
},
7384
{
7485
"crossorigin": "",
75-
"src": "http://localhost:1111/resources/preload/preload-resource/sub2-add/add.sync.js",
86+
"src": "http://localhost:1111/resources/preload/preload-resource/sub2-button/federation-remote-entry.js",
7687
},
7788
],
7889
}
7990
`;
8091

8192
exports[`preload-remote inBrowser > 3 preload with expose config 1`] = `
8293
{
83-
"links": [],
84-
"scripts": [
94+
"links": [
8595
{
86-
"crossorigin": "",
87-
"src": "http://localhost:1111/resources/preload/preload-resource/sub3/federation-remote-entry.js",
96+
"href": "http://localhost:1111/resources/preload/preload-resource/sub3/add.sync.js",
97+
"rel": "preload",
98+
"type": "script",
8899
},
100+
],
101+
"scripts": [
89102
{
90103
"crossorigin": "",
91-
"src": "http://localhost:1111/resources/preload/preload-resource/sub3/add.sync.js",
104+
"src": "http://localhost:1111/resources/preload/preload-resource/sub3/federation-remote-entry.js",
92105
},
93106
],
94107
}
95108
`;
96109

97110
exports[`preload-remote inBrowser > 3 preload with expose config 2`] = `
98111
{
99-
"links": [],
100-
"scripts": [
112+
"links": [
101113
{
102-
"crossorigin": "",
103-
"src": "http://localhost:1111/resources/preload/preload-resource/sub3/federation-remote-entry.js",
114+
"href": "http://localhost:1111/resources/preload/preload-resource/sub3/add.sync.js",
115+
"rel": "preload",
116+
"type": "script",
104117
},
118+
],
119+
"scripts": [
105120
{
106121
"crossorigin": "",
107-
"src": "http://localhost:1111/resources/preload/preload-resource/sub3/add.sync.js",
122+
"src": "http://localhost:1111/resources/preload/preload-resource/sub3/federation-remote-entry.js",
108123
},
109124
],
110125
}

packages/runtime/__tests__/preload-remote.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ function getLinkInfos(): Array<LinkInfo> {
1717
const linkInfos: Array<LinkInfo> = [...links].map((link) => ({
1818
type: link.getAttribute('as') || '',
1919
href: link.getAttribute('href') || '',
20+
rel: link.getAttribute('rel') || '',
2021
}));
2122
return linkInfos;
2223
}

packages/runtime/src/core.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,14 @@ export class FederationHost {
204204
],
205205
HTMLScriptElement | void
206206
>(),
207+
createLink: new SyncHook<
208+
[
209+
{
210+
url: string;
211+
},
212+
],
213+
HTMLLinkElement | void
214+
>(),
207215
// only work for manifest , so not open to the public yet
208216
fetch: new AsyncHook<
209217
[string, RequestInit],

packages/runtime/src/utils/preload.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { createScript } from '@module-federation/sdk';
2-
1+
import { createLink } from '@module-federation/sdk';
32
import {
43
PreloadAssets,
54
PreloadConfig,
@@ -110,32 +109,50 @@ export function preloadAssets(
110109

111110
const fragment = document.createDocumentFragment();
112111
cssAssets.forEach((cssUrl) => {
113-
const cssEl = document.createElement('link');
114-
cssEl.setAttribute('rel', 'preload');
115-
cssEl.setAttribute('href', cssUrl);
116-
cssEl.setAttribute('as', 'style');
117-
fragment.appendChild(cssEl);
112+
const { link: cssEl, needAttach } = createLink(
113+
cssUrl,
114+
() => {},
115+
{
116+
rel: 'preload',
117+
as: 'style',
118+
},
119+
(url: string) => {
120+
const res = host.loaderHook.lifecycle.createLink.emit({
121+
url,
122+
});
123+
if (res instanceof HTMLLinkElement) {
124+
return res;
125+
}
126+
return;
127+
},
128+
);
129+
130+
needAttach && fragment.appendChild(cssEl);
118131
});
119-
document.head.appendChild(fragment);
120132

121133
jsAssetsWithoutEntry.forEach((jsUrl) => {
122-
const { script: scriptEl } = createScript(
134+
const { link: linkEl, needAttach } = createLink(
123135
jsUrl,
124136
() => {
125137
// noop
126138
},
127-
{},
139+
{
140+
rel: 'preload',
141+
as: 'script',
142+
},
128143
(url: string) => {
129-
const res = host.loaderHook.lifecycle.createScript.emit({
144+
const res = host.loaderHook.lifecycle.createLink.emit({
130145
url,
131146
});
132-
if (res instanceof HTMLScriptElement) {
147+
if (res instanceof HTMLLinkElement) {
133148
return res;
134149
}
135150
return;
136151
},
137152
);
138-
document.head.appendChild(scriptEl);
153+
needAttach && document.head.appendChild(linkEl);
139154
});
155+
156+
document.head.appendChild(fragment);
140157
}
141158
}

packages/sdk/src/dom.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,76 @@ export function createScript(
9595
return { script, needAttach };
9696
}
9797

98+
export function createLink(
99+
url: string,
100+
cb: (value: void | PromiseLike<void>) => void,
101+
attrs?: Record<string, any>,
102+
createLinkHook?: (url: string) => HTMLLinkElement | void,
103+
) {
104+
// <link rel="preload" href="script.js" as="script">
105+
106+
// Retrieve the existing script element by its src attribute
107+
let link: HTMLLinkElement | null = null;
108+
let needAttach = true;
109+
const links = document.getElementsByTagName('link');
110+
for (let i = 0; i < links.length; i++) {
111+
const l = links[i];
112+
const linkHref = l.getAttribute('href');
113+
if (linkHref && isStaticResourcesEqual(linkHref, url)) {
114+
link = l;
115+
needAttach = false;
116+
break;
117+
}
118+
}
119+
120+
if (!link) {
121+
link = document.createElement('link');
122+
link.setAttribute('href', url);
123+
124+
if (createLinkHook) {
125+
const createLinkRes = createLinkHook(url);
126+
if (createLinkRes instanceof HTMLLinkElement) {
127+
link = createLinkRes;
128+
}
129+
}
130+
}
131+
132+
if (attrs) {
133+
Object.keys(attrs).forEach((name) => {
134+
if (link) {
135+
link.setAttribute(name, attrs[name]);
136+
}
137+
});
138+
}
139+
140+
const onLinkComplete = (
141+
prev: OnErrorEventHandler | GlobalEventHandlers['onload'] | null,
142+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
143+
event: any,
144+
): void => {
145+
// Prevent memory leaks in IE.
146+
if (link) {
147+
link.onerror = null;
148+
link.onload = null;
149+
safeWrapper(() => {
150+
link?.parentNode && link.parentNode.removeChild(link);
151+
});
152+
if (prev) {
153+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
154+
const res = (prev as any)(event);
155+
cb();
156+
return res;
157+
}
158+
}
159+
cb();
160+
};
161+
162+
link.onerror = onLinkComplete.bind(null, link.onerror);
163+
link.onload = onLinkComplete.bind(null, link.onload);
164+
165+
return { link, needAttach };
166+
}
167+
98168
export function loadScript(
99169
url: string,
100170
info: {

0 commit comments

Comments
 (0)