前端面试刷题必备(性能优化篇)

前端性能优化

前端面试刷题(JS篇):juejin.cn/post/735241…

前端面试刷题必备(手撕代码篇):juejin.cn/post/735607…

前端面试刷题必备(CSS篇):juejin.cn/post/735769…

前端性能优化是一个非常大的体系,可以通过各种各样的方式来实现性能优化,比如:webpack 打包、网络请求、图片压缩、css 优化等等方式。

页面渲染优化

页面渲染的性能优化,大部分人会想到的是输入 URL 到游览器显示页面发生了什么?

这是一个非常大的体系,会从网络请求到游览器渲染,这次我们只讨论游览器渲染的流程:

  1. 解析 HTML 文件,构建 DOM 树,同事主进程去下载 CSS 文件
  2. CSS 文件下载完成后,会解析 CSS 文件成树形的数据结构,结合 DOM 树合并成 RenderObject 树
  3. 这时候会对 RenderObject 树种的元素尺寸、位置等信息进行计算布局
  4. 开始绘制 RenderObject 树的各个属性,如:背景色,透明度等等
  5. 浏览器主进程将默认的图层和复合图层交给 GPU 进程,GPU 进程再将各个图层合成(composite),最后显示出页面

上述过程中,我们可以进行那些性能优化?

Script 标签的优化

我们知道在游览器碰到 script 标签时,如果没有deferasync,游览器就会立即加载并执行对应的 js 文件,就会造成阻塞。
因此我们可以使用deferasync是去异步加载外部的 JS 脚本文件,他们都不会阻塞页面的解析。

deferasync他们有啥差别?
  1. 执行顺序
    • 多个带 async 属性的标签,不能保证加载的顺序
    • 多个带 defer 属性的标签,按照加载顺序执行
  2. 脚本是否并行执行
    • async 属性表示后续文档的加载和执行与 js 脚本的加载和执行是并行进行的,即异步执行
    • defer 属性加载后续文档的过程和 js 脚本的加载时并行进行的,js 脚本需要等元素解析完成后才执行,DOMContentLoaded 时间触发执行之前

游览器的回流和重绘

回流和重绘对游览器的性能消耗都是比较大的,回流必将引起重绘,重绘不一定会引起回流。因为,我们需要尽量避免造成页面的回流。

回流

当元素的尺寸、结构、位置等信息发生变化时,游览器需要重新渲染部分或全部文档,进行重新布局的过程叫做回流。
引起回流的操作有很多:

  • 增删 DOM 元素
  • 游览器窗口大小变化
  • 初次渲染
  • 元素字体变化
  • 元素尺寸位置变化
重绘

当页面中元素的样式属性发生改变但不影响元素的布局位置时,游览器对这个元素的样式重新绘制的过程叫做重绘。

触发重绘的方式也很多:

  • 更改字体颜色
  • 更改背景色
  • 设置透明度
使用 RequestAnimationFrame 函数实现动画

页面中可能会存在一些通过 js 实现的动画效果,我们应该避免使用setTimeout()setInterval()来实现,因为这种回调可能会导致丢失帧而发生卡顿。
requestAnimationFrame是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是按帧对网页进行重绘。显示器有固定的刷新频率(60Hz 或 75Hz),也就是说,每秒最多只能重绘 60 次或 75 次,requestAnimationFrame的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。

CSS3 动画性能优化

页面中可能存在一些 CSS3 的动画属性,我们就可以考虑使用一些优化手段:

  1. 创建一个新的渲染层(减少回流)

    • 有明确的定位属性(relative\fixed\sticky\absolute)
    • 透明度(opacity 小于 1)
    • 有 CSS transfrom 属性(不为 none)
    • 当前有对于 opacity\transform\fliter\backdrop-filter 属性的动画
  2. 创建合成层。合成层会开始 GPU 加速页面渲染,但不能滥用

    • 对 opacity\transform\fliter\backdrop-filter 应用了 animation 或 transition(需要时 active 的 animation 或者 transition)
    • will-change 设置为 opacity\transform\top\left\bottom\right;(提前通知浏览器元素将要做什么动画,让浏览器提前准备合适的优化设置)
    • 有 3D transform 函数:比如 translate3d\scale3d\rotate3d 等

图片优化

图片压缩

其实就是减少图片的体积大小,提高网络请求速度。

精灵图

就是像一些 icon 小图片,我们可以将这些图片集成在一张图片里,通过定位等相关技术实现展示需要的图标。

SVG 替换图片

SVG 是可缩放矢量图形。(矢量:既有大小又有方向的量),可以按比例缩小,并支持压缩。

优点:

  • 放大缩小不会失真,可被许多设备和游览器中使用。
  • 灵活,可以直接使用 js 和 css 操作
  • 可移动话,可以使用 js\css\smil 等方式进行动画
  • 轻量级,与其他格式相比,svg 文件通常较小
  • 可打印,可以以任何分辨率打印不损失图像质量
  • 利于 SEO,因为 svg 文件是 xml 格式的,可以被搜索引擎索引
  • 可压缩,支持压缩
  • 易于编辑

缺点:

  • 不适合高清图片制作,适合用于图标,无法显示与标准图片格式一样多的细节
  • 变得复杂时,加载会比较慢
  • 不完全扩平台

和 canvas 的区别:

  1. 可扩展性:
    • svg 是基于矢量的点、线、形状和数学公式来构建的图形,放大缩小不会失真
    • Canvas 是由一个个像素点构成的图形,放大会模糊
    • SVG 可以再任何分辨率下高质量打印,canvas 不行
  2. 渲染能力:
    • SVG 很复杂的时候渲染能力会变得很慢,因为很大程度上去使用 DOM
    • Canvas 渲染能力很强,适合图像密集型的游戏开发
    • 当图像有大量元素的时候,SVG 文件的大小会增加很快,Canvas 则不会
  3. 灵活度:SVG 可以通过 js 和 css 进行修改,Canvas 只能通过 js 脚本操作
图片懒加载

如果页面中存在大量的图片,一次性全部加载就会变的很慢,因此我们可以让页面先加载一个占位图,然后游览页面的时候,随着可视区域的变化,将原先的占位图替换成真实的图片。

能有效地提高页面加载速度,并且减少服务器负载。

实现方案一:

var imgs = document.querySelectorAll("img");

//offsetTop是元素与offsetParent的距离,循环获取直到页面顶部
function getTop(e) {
  var T = e.offsetTop;
  while ((e = e.offsetParent)) {
    T += e.offsetTop;
  }
  return T;
}

function lazyLoad(imgs) {
  var H = document.documentElement.clientHeight; //获取可视区域高度
  var S = document.documentElement.scrollTop || document.body.scrollTop;
  for (var i = 0; i < imgs.length; i++) {
    if (H + S > getTop(imgs[i])) {
      imgs[i].src = imgs[i].getAttribute("data-src");
    }
  }
}

window.onload = window.onscroll = function () {
  //onscroll()在滚动条滚动的时候触发
  lazyLoad(imgs);
};

实现方案二:

var imgs = document.querySelectorAll("img");

//用来判断bound.top<=clientHeight的函数,返回一个bool值
function isIn(el) {
  var bound = el.getBoundingClientRect();
  var clientHeight = window.innerHeight;
  return bound.top <= clientHeight;
}
//检查图片是否在可视区内,如果不在,则加载
function check() {
  Array.from(imgs).forEach(function (el) {
    if (isIn(el)) {
      loadImg(el);
    }
  });
}
function loadImg(el) {
  if (!el.src) {
    var source = el.dataset.src;
    el.src = source;
  }
}
window.onload = window.onscroll = function () {
  //onscroll()在滚动条滚动的时候触发
  check();
};
WebP

将图片转换为 webp 格式,减少体积提高速度。

节流与防抖

代码实现参考:juejin.cn/post/735607…

防抖

防抖是在时间被触发 n 秒后,再执行回调,在这 n 秒内如果再被触发,则重新计时,像一些点击事件产生的网络请求等等。

防抖的应用场景:

  • 按钮提交,避免多次点击造成多次提交
  • 文本输入查找,避免造成过多的请求
节流

节流则是在一个规定的时间内,只会触发一次事件的回调,像我们的页面滚动等等,可以用节流来控制事件调用的频率。

节流的应用场景:

  • 拖拽:固定时间内执行一次回调,避免超高频率执行
  • 页面滚动:同上
  • 缩放:监控游览器页面变化

打包优化

在我们开发中,我们有很多的打包工具可以使用,像一些 webpack、vite、Gulp 等等,在这里我们以 webpack 为例。

既然说到了打包,那我们先来简单了解一下打包的流程:
webpack 的运行是一个串行的过程

  1. 初始化参数:从配置文件或 shell 语句中读取合并参数,得到最终的参数
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行 run 方法开始编译。
  3. 确定入口:根据配置文件中的 entry 参数找到所有的入口文件。
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
  5. 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

总结就是 3 个阶段:

  1. 初始化:启动构建,读取与合并配置参数,加载Plugin,实力化Compiler
  2. 编译:从 Entry 触发,针对每一个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理
  3. 输出:将编译以后的 Module 组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中

那我们如何利用 webpack 提高打包效率呢,我们此处列举部分内容

webpack 优化-多进程打包

当我们的项目体量逐渐变大的时候,打包就会变得非常漫长,因为他是单线程模式的,只能逐个文件处理。因此开启多进程去打包是非常有必要的。常见的 loader 有thread-loader

使用:只要把 thread-loader 放置在其他 loader 之前, 那 thread-loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行。

webpack 优化-利用缓存(缩短连续构建时间)

webpack 缓存的方法有很多,比如cache-loaderHardSourceWebpackPluginbabel-loadercacheDirectory标志。这些都可以在重新运行期间节约大量的时间,但是在初次运行的时候会比较慢。

cache-loader

使用起来比较简单,在一些性能开销大的 loader 之前添加此 loader

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ["cache-loader", ...loaders],
        include: path.resolve("src"),
      },
    ],
  },
};

HardSourceWebpackPlugin

第一次构建花费正常的时间,第二次构建显著加快

const HardSourceWebpackPlugins = require("hard-source-webpack-plugin");
const clientWebpackConfig = {
  // ...
  plugins: [
    new HardSourceWebpackPlugin({
      // cacheDirectory是在高速缓存写入。默认情况下,将缓存存储在node_modules下的目录中
      // 'node_modules/.cache/hard-source/[confighash]'
      cacheDirectory: path.join(
        __dirname,
        "./lib/.cache/hard-source/[confighash]"
      ),
      // configHash在启动webpack实例时转换webpack配置,
      // 并用于cacheDirectory为不同的webpack配置构建不同的缓存
      configHash: function (webpackConfig) {
        return require("node-object-hash")({ sort: false }).hash(webpackConfig);
      },
      // 当加载器、插件、其他构建时脚本或其他动态依赖项发生更改时,
      // hard-source需要替换缓存以确保输出正确。
      // environmentHash被用来确定这一点。如果散列与先前的构建不同,则将使用新的缓存
      environmentHash: {
        root: process.cwd(),
        directories: [],
        files: ["package-lock.json", "yarn.lock"],
      },
      info: {
        mode: "none",
        level: "debug",
      },
      cachePrune: {
        maxAge: 2 * 24 * 60 * 60 * 1000,
        sizeThreshold: 50 * 1024 * 1024,
      },
    }),
    new HardSourceWebpackPlugins.ExcludeModulePlugin([
      {
        test: /.*\.DS_Store/,
      },
    ]),
  ],
};

webpack 优化-HotModuleReplacement

HMR 也就是热模块替换:在程序运行中,替换、添加、删除模块,而无需重新加载整个页面,从而提高了开发时的构建速度。

    devServer: {
        static: {
          directory: path.join(__dirname, '../public'), // 通过 static.directory 配置项告诉 dev-server 监听文件。默认启用,文件更改将触发整个页面重新加载。可以通过将 watch 设置为 false 禁用。
        },
        client:{
            progress: true, // 在游览器中以百分比显示编译进度
        },
        hot: true, // 启用webpack的热模块替换
        compress: true, // 启用 gzip compression
        host:"localhost", // 启动服务器域名
        port: 9000, // 启动服务器端口号
        open:true, // 自动打开浏览器
    }

webpack 优化-减少代码体积 Tree Shaking

为什么?

开发的时候我们定义了一些工具函数库,或者引入第三方工具函数库或组件。如果没有处理的话,打包时会引入整个库,但是实际上我们可能只是用上极小部分的功能。

这样将整个库都打包进去,体积就太大了。

Tree Shaking是一种术语,通常用于描述一处 Javascript 中没有用上的代码。webpack 已经默认开启该功能,无需其他配置。

注意:它依赖 ES Module

webpack 打包优化总结

我们从 4 个角度对 webpack 和代码进行了优化:

  • 提升开发体验
  1. 使用 Source Map 让开发或上线时代码报错能有更加准确的错误提示。
  • 提升 webpack 提升打包构建速度
  1. 使用 HotModuleReplacement 让开发时只重新编译打包更新变化了的代码,不变的代码使用缓存,从而使更新速度更快。
  2. 使用 OneOf 让资源文件一旦被某个 loader 处理了,就不会继续遍历了,打包速度更快。
  3. 使用 Include/Exclude 排除或只检测某些文件,处理的文件更少,速度更快。
  4. 使用 Cache 对 eslint 和 babel 处理的结果进行缓存,让第二次打包速度更快。
  5. 使用 Thead 多进程处理 eslint 和 babel 任务,速度更快。(需要注意的是,进程启动通信都有开销的,要在比较多代码处理时使用才有效果)
  • 减少代码体积
  1. 使用 Tree Shaking 剔除了没有使用的多余代码,让代码体积更小。
  2. 使用 @babel/plugin-transform-runtime 插件对 babel 进行处理,让辅助代码从中引入,而不是每个文件都生成辅助代码,从而体积更小。
  3. 使用 Image Minimizer 对项目中图片进行压缩,体积更小,请求速度更快。(需要注意的是,如果项目中图片都是在线链接,那么就不需要了。本地项目静态图片才需要进行压缩。)
  • 优化代码运行性能
  1. 使用 Code Split 对代码进行分割成多个 js 文件,从而使单个文件体积更小,并行加载 js 速度更快。并通过 import 动态导入语法进行按需加载,从而达到需要使用时才加载该资源,不用时不加载资源。
  2. 使用 Preload / Prefetch 对代码进行提前加载,等未来需要使用时就能直接使用,从而用户体验更好。
  3. 使用 Network Cache 能对输出资源文件进行更好的命名,将来好做缓存,从而用户体验更好。
  4. 使用 Core-js 对 js 进行兼容性处理,让我们代码能运行在低版本浏览器。
  5. 使用 PWA 能让代码离线也能访问,从而提升用户体验。

后续网络请求的优化以及框架库的优化放置其他章节进行讲述。

原文链接:https://juejin.cn/post/7357909187953311785 作者:沸羊羊你快用力推呀

(0)
上一篇 2024年4月16日 上午10:15
下一篇 2024年4月16日 上午10:26

相关推荐

发表回复

登录后才能评论