首屏加载时间优化的几种解决方案🙋🏿🙋🏿🙋🏿

首屏加载时间是一个衡量网页性能和用户体验的关键指标,这个问题无论是在面试中还是在项目开发中都占有极其高的权重。它指的是从用户开始请求网页到网页的第一屏内容完全渲染完成并对用户可见的时间。首屏 指的是用户不滚动页面时看到的那部分内容,通常是访问网站或应用时最先展示给用户的信息区域。

首屏加载时间的长短直接影响用户的第一印象和留存率,因为用户往往对加载速度较慢的网站或应用有较低的耐心。如果首屏加载时间过长,用户可能会感到不耐烦,甚至在内容完全加载前离开,这样就增加了跳出率,降低了用户体验。

白屏时间是指从用户发起页面请求(比如点击一个链接或在浏览器地址栏输入网址)到页面开始出现第一批可视化内容(不是完全的空白)之间的时间。在这段时间内,用户面对的是一片空白屏幕,因此称之为"白屏时间"。白屏时间主要受网络速度、服务器响应速度和初步渲染所需的资源大小等因素影响。

代码分割

在结合 Webpack 和 React 的项目中,代码分割(Code Splitting)是一种重要的性能优化手段,特别是对于首屏加载时间的优化。代码分割可以将一个大的 bundle 文件拆分成多个小的 chunks(块),这样可以按需加载,减少首次加载的时间,加快首屏显示。

Webpack 的 SplitChunksPlugin 可以用来自动分割公共模块和第三方库。通过合理配置 optimization.splitChunks 选项,可以把公共依赖提取到单独的 chunk 中,避免重复打包,减少首屏加载的体积。

js 复制代码
optimization: {
  splitChunks: {
    chunks: 'all', // 可以是`async`(仅分割异步加载模块),`initial`(仅分割初始加载模块),或`all`(两者都分割)
    minSize: 20000, // 生成chunk的最小体积(以字节为单位)
    minChunks: 1, // 在分割之前,模块被共享的最少次数
    maxAsyncRequests: 30, // 按需加载时的最大并行请求数
    maxInitialRequests: 30, // 入口点的最大并行请求数
    automaticNameDelimiter: '~', // 默认情况下,webpack将使用块的来源和名称生成名称(例如vendors~main.js)
    cacheGroups: { // 缓存组可以继承和/或覆盖splitChunks.*的任何选项
      vendors: {
        test: /[\\/]node_modules[\\/]/,
        priority: -10
      },
      default: {
        minChunks: 2,
        priority: -20,
        reuseExistingChunk: true
      }
    }
  }
}

在 React 中,可以使用动态导入的方法来,利用 import()语法实现动态导入,Webpack 会自动将这些导入的模块分割成新的 chunk。对于 React 组件,可以结合 React.lazy 进行按需加载。

jsx 复制代码
const LazyComponent = React.lazy(() => import("./LazyComponent"));

使用 Webpack 的魔法注释来实现更细致的控制,比如命名 chunk、预加载和预获取。

js 复制代码
const LazyComponent = React.lazy(() =>
  import(/* webpackChunkName: "lazy-component" */ "./LazyComponent")
);
// 预加载
const AnotherComponent = React.lazy(() =>
  import(/* webpackPrefetch: true */ "./AnotherComponent")
);
// 预获取
const YetAnotherComponent = React.lazy(() =>
  import(/* webpackPreload: true */ "./YetAnotherComponent")
);

为动态导入的组件提供加载状态,可以使用 Suspense 组件来包裹懒加载的组件,并指定一个 fallback 加载指示器:

对于使用 React Router 之类的路由库的应用,可以在路由配置中应用代码分割,为每个路由页面实现懒加载:

jsx 复制代码
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import React, { Suspense, lazy } from "react";

const Home = lazy(() => import("./routes/Home"));
const About = lazy(() => import("./routes/About"));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
      </Switch>
    </Suspense>
  </Router>
);

优化资源加载

优化资源加载是提高网页性能、尤其是首屏加载速度的关键方面。资源包括 JavaScript、CSS、图片、字体等。优化这些资源的加载可以显著改善用户体验。

减少请求次数

将多个小的 JavaScript 或 CSS 文件合并成一个文件是一种常见的前端性能优化策略。这种做法可以减少 HTTP 请求的次数,从而提高页面加载速度和用户体验。

为什么要合并文件:

  1. 减少 HTTP 请求:每个文件的请求都涉及到一定的开销(如 DNS 查找、TCP 握手等),特别是在 HTTP/1.x 协议下,这些开销会显著影响加载性能。通过合并文件,可以减少这些请求的次数,从而减少总体开销。

  2. 提高缓存效率:将多个文件合并为一个,可以提高浏览器缓存的利用率。用户在首次访问网站时加载了合并后的文件,之后再次访问或访问其他页面时,如果使用了相同的合并文件,就可以直接从缓存中加载,而不需要再次发起请求。

合并文件时,需要注意文件之间的依赖关系,确保合并后的文件在执行时能保持正确的依赖顺序。对于一些不需要立即执行的 JavaScript 模块或 CSS 文件,可以考虑使用异步加载的方式,避免阻塞页面的渲染。

如果服务器支持 HTTP/2,利用其多路复用功能,可以同时传输多个请求,减少了合并文件的必要性。但即使在 HTTP/2 环境下,合理的文件合并仍然有其优势,尤其是在缓存管理和初始加载性能方面。

对于图片可以合并多个小图片到一个大图集中,并通过 CSS 背景定位来展示特定部分的技术,被称为 CSS 雪碧图(CSS Sprites)。这种方法有效减少了图片请求的次数,提升了页面加载性能。另一种优化手段是将小图标或页面中的小图片转换为 Base64 编码格式并直接嵌入到 HTML 或 CSS 中,进一步减少 HTTP 请求。但需注意,Base64 编码会使图片体积增大约 33%,因此更适用于较小的图片。这两种技术都是前端性能优化的常用方法,旨在加快页面渲染速度和提升用户体验。

使用现代图片格式

WebP 和 AVIF 是相对较新的图像格式,相比传统的 JPEG 和 PNG 格式,它们提供了更高的压缩效率,这意味着在保持相似图像质量的情况下,文件大小更小。这背后的原因主要涉及到这些格式采用的先进编码技术:

webp

先进的压缩算法:WebP 格式使用了基于 VP8 视频编解码器的压缩技术。这种技术针对图像内容进行更高效的压缩,通过减少编码图像块的位数来减小文件大小,同时尽量保持图像质量。

支持无损和有损压缩:WebP 既支持无损压缩,也支持有损压缩。无损压缩 WebP 图像通常比 PNG 小 30%,而有损压缩的 WebP 图像则比 JPEG 小 25-34%,并且可以提供相同甚至更好的质量。

丰富的颜色表现:WebP 支持更广的颜色范围和深度,包括 8 位或 10 位色深的有损编码和 8 位色深的无损编码,这使得它在色彩表现上优于标准的 JPEG 格式。

集成了额外的功能:如支持透明度(Alpha 通道)和动画,这使得 WebP 成为一种非常灵活的图像格式,能够替代 GIF 等格式。

AVIF

基于 AV1 视频编解码器:AVIF 是一种新的图像格式,基于开源的 AV1 视频编解码器。AV1 是由联盟视频联盟开发的,旨在网络上提供高效的视频流传输。

更高效的压缩性能:AVIF 提供了比 WebP 更高的压缩效率,尤其是在高分辨率和高质量图像上。在相同的视觉质量下,AVIF 文件的大小通常比 WebP 更小。

更好的图像质量:AVIF 支持更高的动态范围和颜色深度,包括 HDR(高动态范围图像)和广色域图像,这意味着它可以提供更丰富的色彩和更细腻的图像细节。

更多的编码特性:AVIF 支持多种编码特性,如 10 位和 12 位色深、4:4:4、4:2:2 和 4:2:0 色度子采样等,这为图像提供了更多的灵活性和选择。

总的来说,WebP 和 AVIF 之所以能在保持相同图片质量的同时减少文件大小,主要是因为它们采用了更高效的压缩算法和更先进的编码技术。这些技术使得 WebP 和 AVIF 能够更好地减少冗余信息,从而在不牺牲图像质量的前提下降低文件大小。这些特性使得 WebP 和 AVIF 非常适合用于网络图像,以提高网页加载速度和改善用户体验。

预加载关键资源

在现代网页性能优化中,<link rel="preload"><link rel="prefetch"> 是两种重要的资源提示(Resource Hints),它们能够帮助开发者控制浏览器的资源加载优先级,从而优化用户体验。

<link rel="preload"> 是一种告诉浏览器预先加载页面初始化时即需的资源的方法。这些资源对于当前页面是必要的,但可能由于 HTML 解析的顺序或者延迟加载的策略,并不会立即被加载。通过使用preload,开发者可以显式地告诉浏览器尽早地加载这些关键资源,以确保它们能够及时地被使用,从而减少渲染阻塞时间,加快首屏渲染速度。

html 复制代码
<!-- 预加载关键的CSS文件 -->
<link rel="preload" href="important.css" as="style" />

<!-- 预加载关键的JavaScript文件 -->
<link rel="preload" href="main.js" as="script" />

<!-- 预加载字体文件 -->
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin />

在使用preload时,as属性非常重要,因为它指定了被加载资源的类型,这有助于浏览器正确地优先加载资源,并使用正确的加载策略。

<link rel="prefetch"> 是另一种资源提示,它指示浏览器在空闲时间预先获取用户将来可能需要的资源。这通常适用于预加载下一页面的资源,例如用户很可能会点击的链接的资源。prefetch的优先级较低,不会影响当前页面的关键资源加载,因此它主要用于提升未来页面的加载速度。

html 复制代码
<!-- 预获取下一页面可能需要的CSS文件 -->
<link rel="prefetch" href="next-page-style.css" />

<!-- 预获取下一页面可能需要的JavaScript文件 -->
<link rel="prefetch" href="next-page-script.js" />
  • <link rel="preload">:用于当前页面的关键资源,目的是尽快加载这些资源以提升当前页面的性能。适用于那些对当前页面渲染至关重要的资源,比如关键的 CSS 和 JavaScript 文件、字体文件等。
  • <link rel="prefetch">:用于将来的页面资源,目的是利用浏览器的空闲时间提前获取可能需要的资源,以提升未来页面的加载速度。适用于那些用户可能接下来会访问的页面资源,如下一篇文章的脚本、样式表或图片等。

通过合理利用这两种资源提示,开发者可以更好地优化资源的加载顺序和时间,从而改善用户体验。不过,需要注意的是,过度使用或不当使用这些技术可能会带来资源竞争或浪费带宽,因此应该根据实际情况谨慎选择使用。

优化 CSS 加载

优化 CSS 加载是提高页面渲染速度和用户体验的重要方面。主要包括两个策略:处理关键 CSS(Critical CSS)和异步加载非关键 CSS。

关键 CSS 是指用于首屏渲染的最小 CSS 集合,即在页面加载的初始阶段用于样式化内容的 CSS。将关键 CSS 内联在 HTML 文档的<head> 部分中可以减少阻塞渲染的外部样式表的数量,从而加快首屏内容的显示速度。

关键 CSS

在使用 Webpack 构建工具时,可以通过特定的插件和加载器来自动提取关键 CSS,以优化页面加载性能。下面是在 Webpack 环境中提取关键 CSS 的一些常用方法和步骤:

使用 html-webpack-plugin 和 critters-webpack-plugin:

  1. 安装相关插件 critters-webpack-plugin 和 html-webpack-plugin。

  2. 在 webpack.config.js 文件中,引入这两个插件并将它们添加到 plugins 数组中。critters-webpack-plugin 会处理 html-webpack-plugin 生成的 HTML 文件,提取并内联关键 CSS。

js 复制代码
const HtmlWebpackPlugin = require("html-webpack-plugin");
const Critters = require("critters-webpack-plugin");

module.exports = {
  // 其他Webpack配置...
  plugins: [
    new HtmlWebpackPlugin({
      template: "src/index.html",
    }),
    new Critters({
      // Critters的配置选项...
    }),
  ],
};

对于更复杂的场景,比如当你需要进一步优化并清理未使用的 CSS 时,可以结合使用 mini-css-extract-plugin 和 purifycss-webpack。

js 复制代码
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const PurifyCSSPlugin = require("purifycss-webpack");
const glob = require("glob");

module.exports = {
  // 其他Webpack配置...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].[contenthash].css",
    }),
    new PurifyCSSPlugin({
      paths: glob.sync(path.join(__dirname, "src/**/*.html")),
      minimize: true,
    }),
  ],
};
非关键 CSS

非关键 CSS 是指对首屏渲染不是必需的 CSS,例如用于页面下方内容或其他页面的样式。异步加载这些非关键 CSS 可以确保它们不会阻塞首屏的渲染。

主要有以下几种实现方法:

  1. 使用标签的 media 属性:通过设置 media 属性为一个不匹配的媒体查询,可以让浏览器延迟加载 CSS 文件。页面加载完成后,通过 JavaScript 更改 media 属性为 all 来加载和应用样式。

    html 复制代码
    <link
      rel="stylesheet"
      href="non-critical.css"
      media="print"
      onload="this.media='all'"
    />
    <noscript><link rel="stylesheet" href="non-critical.css" /></noscript>
  2. 使用 JavaScript 动态加载 CSS:通过 JavaScript 动态创建 <link> 标签并插入到 <head> 中,可以实现 CSS 的异步加载。

    js 复制代码
    var link = document.createElement("link");
    link.rel = "stylesheet";
    link.href = "non-critical.css";
    document.head.appendChild(link);
  3. 减少 CSS 选择器的复杂性:避免使用过于复杂的 CSS 选择器,因为它们会增加浏览器的样式计算时间。

  4. 避免使用@import:@import 允许在 CSS 文件中导入其他 CSS 文件,但它会增加页面加载时间,因为浏览器需要解析第一个 CSS 文件后才能发现并加载导入的文件。

优化 JavaScript 加载

使用 async 和 defer 属性异步加载 JavaScript,减少阻塞渲染的脚本。

当脚本带有 async 属性时,它将并行于页面的加载进行下载。一旦脚本下载完成,它会立即执行,而不用等待其他脚本或页面渲染完成。这对于那些不依赖于其他脚本且不被其他脚本依赖的功能来说是个不错的选择。

js 复制代码
<script async src="script.js"></script>

带有 defer 属性的脚本会等到整个页面都解析完毕后才会执行,而且是按照它们在文档中出现的顺序依次执行的。这通常用于那些需要访问或修改 DOM 的脚本,且不影响文档的初始显示。

js 复制代码
<script defer src="script.js"></script>

DNS 预解析

DNS 预解析是一种性能优化技术,用于减少用户浏览网页时遇到的延迟。当用户访问一个网页时,浏览器需要将网页上的域名(如 example.com)转换成 IP 地址,这个过程称为 DNS 解析。DNS 解析需要时间,特别是当 DNS 查询需要通过多个网络节点时,这个时间可能会显著影响网页的加载速度。通过使用 DNS 预解析,可以提前进行这个解析过程,从而减少用户等待时间。

DNS 解析可能需要一定的时间,特别是如果 DNS 服务器距离用户较远,或者需要多次跳转才能找到相应的记录时。通过预解析,可以在用户实际请求资源之前完成这一步骤,从而减少等待时间。

DNS 预解析可以通过在 HTML 中添加 标签来实现,如下所示:

html 复制代码
<link rel="dns-prefetch" href="//www.难道你是个天才?.com" />

DNS 预解析对于那些含有大量来自不同域名资源的网页尤为有用,比如外部脚本、广告、图片等

其他的一些方案

使用 CDN 是一种比较常见的解决方案,将资源部署到 CDN 上,可以让资源从用户地理位置最近的服务器加载,减少延迟。

另外一个就是使用 HTTP/2,因为 HTTP/2 提供了头部压缩、服务器推送、请求复用等特性,相较于 HTTP/1.1 可以显著提升资源加载效率。

编写出高性能且易维护的代码极其重要,例如减少回来和重绘等等。(之前实习看到一个同事在一个 CSS 文件上写了 3000 多行样式,看的时候还是在开发 ing...)

终极解决方案

让用户换电脑,如果响应慢,肯定是电脑出了问题,我的建议是换成顶配的 mac。(难道你是个天才?)

总结

通过实施上面的这些策略,我们可以优化资源加载过程,提升网页加载速度和用户体验。值得注意的是,每个项目的具体情况不同,因此在实施这些优化时应根据实际需要和测试结果灵活调整。

最后分享两个我的两个开源项目,它们分别是:

来都来了,不来个 star 再走吗 🚗🚗🚗

相关推荐
前端毕业班6 分钟前
uni-app 小程序支持 teleport 了
前端·javascript·vue.js
东风破_34 分钟前
LLM 为什么记不住你?无状态、messages 和上下文管理
javascript·人工智能
梨子同志43 分钟前
qiankun
前端
梨子同志1 小时前
Nginx
前端
梨子同志1 小时前
NestJS
前端
梨子同志1 小时前
Next.js
前端
谭光志1 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
Hilaku1 小时前
前端架构师的克制:什么时候该坚决对新技术说不?
前端·javascript·程序员
陈随易2 小时前
前端项目部署只要30秒
前端·后端·程序员
本末倒置1832 小时前
从 Vue CLI 迁移到 Rsbuild 后,JSX 突然全挂了?一次排查记录
前端