一以贯之,从源码的角度去看,懒加载是什么。
懒加载是什么?
懒加载是一种在页面加载时延迟加载一些非关键资源的技术,换句话说就是按需加载。
我们之前看到的懒加载一般是这样的形式:
- 浏览一个网页,准备往下拖动滚动条
- 拖动一个占位图片到视窗
- 占位图片被瞬间替换成最终的图片
为什么使用懒加载而不直接加载?
- 浪费流量。在不计流量收费的网络,这可能不重要;在按流量收费的网络中,毫无疑问,一次性加载大量图片就是在浪费用户的钱。
- 消耗额外的电量和其他的系统资源,并且延长了浏览器解析的时间。因为媒体资源在被下载完成后,浏览器必须对它进行解码,然后渲染在视窗上,这些操作都需要一定的时间。
懒加载图片和视频,可以减少页面加载的时间、页面的大小和降低系统资源的占用,这些对于性能都有显著地提升。总体来讲,就是改善用户体验,增强页面性能。
再牵涉一条术语:代码分割
代码分割
(1)为什么要进行代码分割?
现在前端项目基本都采用打包技术,比如 Webpack,JS逻辑代码打包后会产生一个 bundle.js 文件,而随着我们引用的第三方库越来越多或业务逻辑代码越来越复杂,相应打包好的 bundle.js 文件体积就会越来越大,因为需要先请求加载资源之后,才会渲染页面,这就会严重影响到页面的首屏加载。
而为了解决这样的问题,避免大体积的代码包,我们则可以通过技术手段对代码包进行分割,能够创建多个包并在运行时动态地加载。现在像 Webpack、 Browserify等打包器都支持代码分割技术。
(2)什么时候应该考虑进行代码分割?
这里举一个平时开发中可能会遇到的场景,比如某个体积相对比较大的第三方库或插件(比如JS版的PDF预览库)只在单页应用(SPA)的某一个不是首页的页面使用了,这种情况就可以考虑代码分割,增加首屏的加载速度。
懒加载 示例代码
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}> <OtherComponent /> </Suspense>
</div>
);
}
通过 import()
、React.lazy
和 Suspense
共同一起实现了 React 的懒加载,核心还是 import() 的功能。调用import()函数将会返回一个promise对象。
function importModule(url) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
const tempGlobal = "__tempModuleLoadingVariable" + Math.random().toString(32).substring(2);
script.type = "module";
script.textContent = `import * as m from "${url}"; window.${tempGlobal} = m;`;
script.onload = () => {
resolve(window[tempGlobal]);
delete window[tempGlobal];
script.remove();
};
script.onerror = () => {
reject(new Error("Failed to load module script with URL " + url));
delete window[tempGlobal];
script.remove();
};
document.documentElement.appendChild(script);
});
}
接下来JS通过将import函数作为参数的形式来实现需要时加载。
React懒加载源码
export function lazy<T>(
ctor: () => Thenable<{default: T, ...}>,
): LazyComponent<T, Payload<T>> {
const payload: Payload<T> = {
// We use these fields to store the result.
_status: Uninitialized,
_result: ctor,
};
const lazyType: LazyComponent<T, Payload<T>> = {
$$typeof: REACT_LAZY_TYPE,
_payload: payload,
_init: lazyInitializer,
};
if (__DEV__) {
......
}
return lazyType;
}
上面的代码返回lazyType
,在lazyType
中调用_init
属性方法,在lazyInitializer
函数中调用import()
参数。
function lazyInitializer<T>(payload: Payload<T>): T {
if (payload._status === Uninitialized) {
const ctor = payload._result;
// 加载组件
const thenable = ctor();
// Transition to the next state.
// This might throw either because it's missing or throws. If so, we treat it
// as still uninitialized and try again next time. Which is the same as what
// happens if the ctor or any wrappers processing the ctor throws. This might
// end up fixing it if the resolution was a concurrency bug.
// 对加载组件是否成功的处理:处理详情见上述描述。
// import()返回的是一个promise对象。
thenable.then(
moduleObject => {
if (payload._status === Pending || payload._status === Uninitialized) {
// Transition to the next state.
const resolved: ResolvedPayload<T> = (payload: any);
resolved._status = Resolved;
resolved._result = moduleObject;
}
},
error => {
if (payload._status === Pending || payload._status === Uninitialized) {
// Transition to the next state.
const rejected: RejectedPayload = (payload: any);
rejected._status = Rejected;
rejected._result = error;
}
},
);
if (payload._status === Uninitialized) {
// In case, we're still uninitialized, then we're waiting for the thenable
// to resolve. Set it as pending in the meantime.
const pending: PendingPayload = (payload: any);
pending._status = Pending;
pending._result = thenable;
}
}
if (payload._status === Resolved) {
const moduleObject = payload._result;
if (__DEV__) {
if (moduleObject === undefined) {
console.error(
'lazy: Expected the result of a dynamic imp' +
'ort() call. ' +
'Instead received: %s\n\nYour code should look like: \n ' +
// Break up imports to avoid accidentally parsing them as dependencies.
'const MyComponent = lazy(() => imp' +
"ort('./MyComponent'))\n\n" +
'Did you accidentally put curly braces around the import?',
moduleObject,
);
}
}
if (__DEV__) {
if (!('default' in moduleObject)) {
console.error(
'lazy: Expected the result of a dynamic imp' +
'ort() call. ' +
'Instead received: %s\n\nYour code should look like: \n ' +
// Break up imports to avoid accidentally parsing them as dependencies.
'const MyComponent = lazy(() => imp' +
"ort('./MyComponent'))",
moduleObject,
);
}
}
return moduleObject.default;
} else {
throw payload._result;
}
}
Suspense
Suspense源码(先去理解fiber才能有效阅读Suspense源码),我们依然可以先获取数据,而且可以给上面流程的 2、3 步骤调换顺序:
- 开始获取数据
- 开始渲染
- 结束获取数据
有了 Suspense,我们不必等到数据全部返回才开始渲染。实际上,我们是一发送网络请求,就马上开始渲染:
// 这不是一个 Promise。这是一个支持 Suspense 的特殊对象。
const resource = fetchProfileData();
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline />
</Suspense>
</Suspense>
);
}
function ProfileDetails() {
// 尝试读取用户信息,尽管信息可能未加载完毕
const user = resource.user.read(); return <h1>{user.name}</h1>;
}
function ProfileTimeline() {
// 尝试读取博文数据,尽管数据可能未加载完毕
const posts = resource.posts.read(); return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
- 我们一开始就通过
fetchProfileData()
发出请求。这个方法返回给我们一个特殊的对象“resource”,而不是一个 Promise。在现实的案例中,这个对象是由 Relay 通过集成了 Suspense 来提供的。 - React 尝试渲染
<ProfilePage>
。该组件返回两个子组件:<ProfileDetails>
和<ProfileTimeline>
。 - React 尝试渲染
<ProfileDetails>
。该组件调用了resource.user.read()
,但因为读取的数据还没被获取完毕,所以组件会处于一个“挂起”的状态。React 会跳过这个组件,继续渲染组件树中的其他组件。 - React 尝试渲染
<ProfileTimeline>
。该组件调用了resource.posts.read()
,和上面一样,数据还没获取完毕,所以这个组件也是处在“挂起”的状态。React 同样跳过这个组件,去渲染组件树中的其他组件。 - 组件树中已经没有其他组件需要渲染了。因为
<ProfileDetails>
组件处于“挂起”状态,React 则是显示出距其上游最近的<Suspense>
fallback:<h1>Loading profile...</h1>
。渲染到这里就结束了。
这里的 resource
对象表示尚未存在但最终将被加载的数据。当我们调用 read()
的时候,我们要么获取数据,要么组件处于“挂起”状态。
随着更多数据的到来,React 将尝试重新渲染,并且每次都可能渲染出更加完整的组件树。 当 resource.user
的数据获取完毕之后,<ProfileDetails>
组件就能被顺利渲染出来,这时,我们就不再需要展示 <h1>Loading profile...</h1>
这个 fallback 了。当我们拿到全部数据之后,所有的 fallbacks 就都可以不展示了。
这意味着一个有趣的事实,即使我们使用 GraphQL 客户端来收集单个请求中需要的所有数据,流式响应也可以使我们尽早显示更多的内容。在数据获取时(render-as-we-fetch) (而不是全部数据获取后)渲染,因此,如果 user
在响应中比 posts
出现得更早,我们则可以在响应结束之前“解锁”外层的 <Suspense>
边界。我们之前并没有意识到这一点,即便是 fetch-then-render(接收到全部数据之后渲染)这个解决方案,在数据获取和渲染之间也有“瀑布”问题。Suspense 没有这个“瀑布”问题,像 Relay 这样的库就利用了这个优势。