前端性能优化(全面提升指南)

226 阅读17分钟

本文旨在从输入URL到页面加载完成的过程来建立一个较全面的前端性能优化的体系。

在展开性能优化的话题之前,先抛出一个经常提起的面试题:

从输入URL到页面加载完成,发生了什么

DNS解析:浏览器首先解析URL中的域名,查询域名对应的IP地址。如果在本地缓存中找到了对应的IP地址,就会直接跳过DNS解析的过程。
TCP连接:浏览器通过IP地址与Web服务器建立TCP连接,发送HTTP请求。
发送HTTP请求:浏览器向Web服务器发送HTTP请求,请求页面的HTML代码以及相关的CSSJavaScript、图片等资源。
服务器处理请求:Web服务器接收到请求后,根据请求的路径和参数,处理请求并返回相应的资源。
接收响应:浏览器接收到服务器返回的响应,根据响应头中的内容判断返回的是HTML代码、CSSJavaScript、图片等资源,并按顺序加载解析。
HTML解析:浏览器从返回的HTML代码中解析出文档结构,生成DOM树。
CSS解析:浏览器解析HTML中的样式和外部CSS文件中的样式,生成CSSOM树。
构建渲染树:浏览器将DOM树和CSSOM树组合,生成渲染树。
布局:浏览器根据渲染树计算出每个节点在屏幕上的大小、位置等布局信息。
绘制:浏览器根据布局信息将每个节点绘制成屏幕上的像素。
JavaScript执行:如果HTML中包含JavaScript代码,浏览器会解析并执行JavaScript代码,JavaScript代码执行完成后,页面才算完全加载完成。
综上所述,从输入URL到页面加载完成,涉及到DNS解析、TCP连接、发送HTTP请求、服务器处理请求、接收响应、HTML解析、CSS解析、构建渲染树、布局、绘制、JavaScript执行和完成等步骤。

我们要做的事情,就是针对这些过程进行分解,各个提问,最终构成我们完整的前端性能优化思维。

比如说,DNS解析花时间,要如何优化?

针对DNS优化我们可以开启DNS缓存和DNS预解析

<link rel="dns-prefetch" href="//example.com">

TCP三次握手耗费时间,如何解决? 有长连接、预连接等解决方案。

如果说上述问题需要服务端工程师协助,那HTTP请求与浏览器端的性能优化就是前端工程师可以大展拳脚的地方。

1. 使用CDN

定义:CDN(Content Delivery Network,内容分发网络)是一种通过在全球范围内的多个节点上缓存和分发静态内容来加速网站访问速度和提高性能的技术。

工作原理:

  1. 静态内容缓存:CDN将网站的静态内容缓存在全球范围内的多个节点上。
  2. 负载均衡:用户发送请求时,CDN的负载均衡服务器会选择最近的节点,并将用户请求路由到该节点。
  3. 内容分发:当用户请求到达所选的CDN节点时,节点会从缓存中获取所需的内容,并将其返回给用户。
  4. 如果最近的服务器没有缓存的数据会向上级请求数据 缓存到就近CDN服务器上

优点:

  1. 将静态资源(如图片、CSS和JavaScript文件)缓存在CDN上可以减少服务器的负载和响应时间,提高网站的性能和速度。
  2. 压缩资源:使用CDN的压缩功能可以将静态资源压缩为更小的大小,从而减少网络传输的时间和成本。
  3. 使用负载均衡:使用负载均衡可以将流量分布到多个服务器上,从而提高网站的性能和可用性。

2. 使用缓存

2.1 使用浏览器缓存

浏览器缓存是一种在浏览器中保存静态文件的机制。它可以减少网站的加载时间和带宽使用,提高网站的性能和用户体验。

浏览器缓存有两种类型:强缓存和协商缓存。

  1. 强缓存

强缓存是指在浏览器中缓存资源,并设置一个过期时间或一个相对时间,以决定何时需要重新请求资源。强缓存不会向服务器发送任何请求,而是直接从本地缓存中加载资源。强缓存有两种方式:

  • Expires:过期时间是一个绝对时间,由服务器返回一个过期时间戳,在此时间之前,浏览器可以直接从缓存中加载资源。但是,Expires是以服务器时间为准,如果服务器时间和客户端时间不同步,就会出现错误。
  • Cache-Control:优先级较Expires更高。过期时间是一个相对时间,由服务器返回一个max-age值,表示资源可以缓存的最长时间(单位为秒),在此时间之前,浏览器可以直接从缓存中加载资源。Cache-Control是以客户端时间为准,可以解决Expires的问题,并支持更多的缓存控制选项,如no-cache、no-store、public、private等。

推荐使用Cache-Control

  1. 协商缓存

当强缓存失效后,会使用协商缓存。协商缓存是指在浏览器中缓存资源,并在每次请求时与服务器进行验证,以决定是否需要重新获取资源。协商缓存不会从本地缓存中直接加载资源,而是向服务器发送一个请求头,包含上一次请求时服务器返回的ETag或Last-Modified等信息。服务器根据这些信息决定是否需要返回新的资源,如果资源没有发生变化,服务器会返回一个304 Not Modified响应,浏览器会从缓存中加载资源。

协商缓存有两种方式:

  • ETag:是一个资源的唯一标识符,由服务器返回,在每次请求时都会与服务器进行比较,如果一致,表明资源没有发生变化,服务器会返回一个304 Not Modified响应。
  • Last-Modified:是一个资源的最后修改时间,由服务器返回,在每次请求时都会与服务器进行比较,如果资源没有发生变化,服务器会返回一个304 Not Modified响应。

如果服务器同时使用了ETag和Last-Modified,浏览器会优先使用ETag,因为它可以更准确地判断资源是否发生变化。

如果需要强制浏览器重新请求已经缓存的文件,可以通过以下方法:

1.  修改文件名:可以在URL中添加版本号或者时间戳等,让浏览器认为这是一个新的文件,从而重新请求。
1.  修改文件内容:可以在文件中添加注释或者空格等无意义字符,让浏览器认为这是一个新的文件,从而重新请求。
1.  使用HTTP响应头:可以在HTTP响应头中设置Cache-ControlExpires字段,将缓存时间设置为0,强制浏览器重新请求。例如,设置Cache-Control:max-age=0或者Expires:0。

需要注意的是,强制浏览器重新请求文件会增加网络请求和服务器负载,应该谨慎使用。可以在开发环境下使用这些方法,以便及时获取最新的文件,而在生产环境下,应该使用合适的缓存策略,尽量避免不必要的网络请求和服务器负载。

2.2 使用本地缓存

浏览器本地缓存是浏览器在本地磁盘中保存资源文件的机制,是一种持久化的缓存。

浏览器本地缓存有两种类型:localStorage和sessionStorage。

  1. localStorage

localStorage是一种持久化的本地缓存,可以在浏览器关闭后仍然保存在本地磁盘中,直到用户手动清除缓存或者缓存过期。localStorage存储的数据大小为5MB左右,可以用于保存长期有效的数据,如用户的登录状态、用户偏好设置等。

localStorage的使用方法如下:

// 存储数据
localStorage.setItem('key', 'value');

// 获取数据
var value = localStorage.getItem('key');

// 删除数据
localStorage.removeItem('key');

// 清除所有数据
localStorage.clear();

  1. sessionStorage

sessionStorage是一种会话级别的本地缓存,它会在浏览器会话结束(即关闭浏览器标签页或窗口)时自动清除。sessionStorage存储的数据大小为5MB左右,可以用于保存临时的会话数据,如表单数据、浏览记录等。

sessionStorage的使用方法与localStorage类似。

注意:网站应该考虑到缓存的安全性和隐私性,避免将敏感数据存储在本地缓存中。

2.2 使用Service Worker

Web Workers是一种JavaScript API,可以在浏览器中创建一个独立的执行线程,用于执行一些计算密集型任务,避免阻塞主线程,提高页面响应速度。

在主线程中,我们首先创建了一个新的Web Worker,并通过onmessage方法监听Web Worker返回的消息。然后,我们向Web Worker发送消息,通过postMessage方法发送数据到Web Worker中。

在Web Worker中,我们通过onmessage方法监听主线程发送的消息,并执行计算密集型任务。完成计算任务后,我们通过postMessage方法将结果返回给主线程。

需要注意的是,在Web Worker中不能直接访问DOM元素,也不能访问主线程中的变量和函数。但是,我们可以通过postMessage方法和onmessage方法实现Web Worker与主线程之间的数据传递和通信。

在主线程中:

// 创建一个新的Web Worker
const worker = new Worker('worker.js');

// 监听Web Worker返回的消息
worker.onmessage = function(event) {
  console.log('Received message from worker:', event.data);
};

// 向Web Worker发送消息
worker.postMessage('Hello from the main thread!');

在Web Worker中(worker.js文件):

// 监听主线程发送的消息
onmessage = function(event) {
  console.log('Received message from main thread:', event.data);

  // 执行计算密集型任务
  const result = doSomeHeavyCalculations(event.data);

  // 将结果返回给主线程
  postMessage(result);
};

// 计算密集型任务
function doSomeHeavyCalculations(data) {
  // ...
  return result;
}


3. gzip压缩

gzip压缩的原理是将文件中的重复内容替换为指向相同内容的引用,从而减小文件大小。

在浏览器端,当请求一个经过gzip压缩的文件时,浏览器会在请求头中添加Accept-Encoding: gzip,告诉服务器它支持gzip压缩。服务器在返回文件之前,检查浏览器的请求头中是否包含gzip压缩支持,如果支持则将文件进行gzip压缩并返回给浏览器,否则直接返回未压缩的文件。

在前端开发中,可以使用webpack等工具自动对资源文件进行gzip压缩。

以下是webpack使用gzip压缩的具体步骤:

  1. 在webpack配置文件中,添加compression-webpack-plugin插件。
const CompressionPlugin = require('compression-webpack-plugin')

module.exports = {
  plugins: [
    new CompressionPlugin({
      algorithm: 'gzip'
    })
  ]
}

  1. 对需要进行gzip压缩的文件进行配置。
module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|css|html|svg)$/,
        use: [
          {
            loader: 'gzip-loader',
            options: {
              // gzip压缩级别,可以是1-9的数字,默认为9
              // 压缩级别越高,压缩率越大,但是解压缩时间也越长
              level: 9,
              // 只压缩比这个大小大的文件,单位为字节,默认为0
              threshold: 1024,
              // 对于所有匹配该模式的文件,使用gzip-loader进行压缩
              // 这里使用了排除法,表示不压缩以".min.js"和".min.css"结尾的文件
              exclude: /.*\.min\.(css|js)$/i
            }
          }
        ]
      }
    ]
  }
}

还需要后端做一下配置,这里后端以nginx为例,Nginx 开启

# 开启
gzip on;

# 压缩等级,1-9。设置多少可以参考:http://serverfault.com/questions/253074/what-is-the-best-nginx-compression-gzip-level
gzip_comp_level 2;

# "MSIE [1-6]\." 比如禁止 IE6 使用 GZIP
gzip_disable regex ...

# 最小压缩文件长度
gzip_min_length 20;

# 使用 GZIP 压缩的最小 HTTP 版本
gzip_http_version 1.1;

# 压缩的文件类型,值是 [MIME type](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Complete_list_of_MIME_types)
gzip_types text/html;

4. 图片优化

  • 使用雪碧图
  • 使用字体图标
  • 压缩图片大小:可以使用图片压缩工具将图片大小减小到合适的尺寸。
  • 图片懒加载

在页面中,先不给图片设置路径,只有当图片出现在浏览器的可视区域时,才去加载真正的图片,这就是延迟加载。对于图片很多的网站来说,一次性加载全部图片,会对用户体验造成很大的影响,所以需要使用图片延迟加载。

首先可以将图片这样设置,在页面不可见时图片不会加载:

<img class="lazy-img" data-src="image.jpg" alt="Image">

等页面可见时,使用 JS 加载图片:

// 获取所有需要懒加载的图片
const lazyImages = document.querySelectorAll('.lazy-img');

// 加载图片
function loadImage(image) {
  const src = image.getAttribute('data-src');
  if (!src) {
    return;
  }

  // 创建一个Image对象
  const img = new Image();

  // 加载图片
  img.src = src;

  // 图片加载成功后,将图片的src属性设置为data-src属性
  img.onload = () => {
    image.setAttribute('src', src);
  };

  // 图片加载失败时,显示一个占位图或错误信息
  img.onerror = () => {
    image.setAttribute('src', 'placeholder.jpg');
  };
}

// 监听窗口滚动事件
window.addEventListener('scroll', () => {
  // 遍历所有需要懒加载的图片
  lazyImages.forEach((image) => {
    // 判断图片是否在可视区域内
    if (image.getBoundingClientRect().top <= window.innerHeight) {
      // 加载图片
      loadImage(image);
    }
  });
});

  • 响应式图片

响应式图片是一种根据不同设备分别提供不同尺寸和分辨率的图片的技术。这样可以在不同设备上提供最适合的图片,减小页面加载时间和网络带宽。

通过 picture 实现

<picture>
	<source srcset="banner_w1000.jpg" media="(min-width: 801px)">
	<source srcset="banner_w800.jpg" media="(max-width: 800px)">
	<img src="banner_w800.jpg" alt="">
</picture>
复制代码

通过 @media 实现

@media (min-width: 769px) {
	.bg {
		background-image: url(bg1080.jpg);
	}
}
@media (max-width: 768px) {
	.bg {
		background-image: url(bg768.jpg);
	}
}
  • 使用 webp 格式的图片

WebP是一种由Google开发的新型图片格式,它具有更优的图像数据压缩算法,可以在保证图片质量的同时减小图片大小。对于支持WebP格式的浏览器,可以使用WebP格式的图片来减少页面加载时间。

5. 按需引入/懒加载

  • 图片懒加载 :详见上文。
  • 路由懒加载 : Vue 配置示例:
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'home',
      component: () => import('./views/Home.vue')
    },
    {
      path: '/about',
      name: 'about',
      component: () => import('./views/About.vue')
    }
  ]
})

  • 组件按需引入
  1. 安装 babel-plugin-component

  2. 在 babel 的配置文件中配置 babel-plugin-component

// .babelrc
{
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui", // 按需加载的库名称
        "styleLibraryName": "theme-chalk" // 按需加载的样式文件名称
      }
    ]
  ]
}

  1. 在需要使用组件的地方引入组件
import { Button, Select } from 'element-ui'

Vue.component(Button.name, Button)
Vue.component(Select.name, Select)

6. webpack优化

6.1 升级webpack5

-   开箱即用的持久化缓存
-   优雅的资源处理模块
-   打包体积优化

6.2 使用 Tree Shaking

Tree Shaking 可以将没有使用的代码从打包文件中删除,从而减小打包文件的体积。 在 Webpack 中,使用 Tree Shaking 可以通过配置 "optimization" 属性来实现,设置 mode 为 production 模式,并在 package.json 文件中设置 sideEffects 为 false,排除无副作用的文件,如下:

// webpack.config.js
module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true,
    sideEffects: false,
  },
};

6.3 使用缓存

Webpack 在打包时会对文件进行处理,如果文件没有发生改变,Webpack 会重新打包这些文件,这将导致打包时间变长。使用缓存可以缩短打包时间,提高效率。在 Webpack 中,可以通过配置 cache 属性来实现,如下:

// webpack.config.js
module.exports = {
  cache: true,
};

也可以使用 hard-source-webpack-plugin 插件或 cache-loader 插件来进行缓存配置

6.4 优化 loader

  • Webpack 打包时,只有一个进程在工作,多核 CPU 的性能无法得到充分利用。可以使用 happypack 插件或 thread-loader 插件将多个进程分配到多个 CPU 核心上,加快打包速度。
  • 为babel-loader开启缓存,在使用 loader 进行模块转换时,需要避免无用的转换操作。
  • 可以通过在 loader 配置中设置 excludeinclude 属性,来限制 loader 的作用范围,减少不必要的转换操作。

通过exclude排除node_module

6.5 使用 DLLPlugin

DllPlugin 是一个将第三方库(如 React、Vue)单独打包的工具,可以提高打包速度。使用 DllPlugin 需要先运行一次 Webpack 将第三方库打包成 dll 文件,然后在开发中引用 dll 文件,而不是直接引用第三方库。

6.6 优化打包输出

  • 我们在生产环境构建的config文件中使用webpack-bundle-analyzer来分析打包体积
  • 将打包输出的文件进行压缩、混淆等优化操作,可以减小文件体积,提高加载速度。可以使用 compression-webpack-plugin 插件进行压缩操作,使用 terser-webpack-plugin 插件进行混淆操作。

6.7 splitChunks提取公共代码

SplitChunks插件是webpack中用来提取或分离代码的插件,主要作用是提取公共代码,减少代码被重复打包,拆分过大的js文件,合并零散的js文件

7. 使用服务端渲染

客户端渲染: 获取 HTML 文件,根据需要下载 JavaScript 文件,运行文件,生成 DOM,再渲染。

服务端渲染:服务端返回 HTML; 客户端渲染: 获取 HTML 文文件,客户端只需解析 HTML。

  • 优点:首屏渲染快,SEO 好。
  • 缺点:配置麻烦,增加了服务器的计算压力。

8. 代码层面优化

前端代码层面的优化是一项细致、耐心的工作,需要结合实际情况,针对性地制定优化策略,通过不断的优化实践,以提高页面性能,提升用户体验。

8.1 减少重排重绘

重排

当DOM变化影响了元素的几何属性(宽、高改变等等),浏览器此时需要重新计算元素几何属性,并且页面中其他元素的几何属性可能会受影响这样渲染树就发生了改变,也就是重新构造RenderTree渲染树。

重绘

布局没有发生改变,改变那些不会影响元素在网页中的位置的元素样式时,譬如background-color(背景色), border-color(边框色), visibility(可见性),浏览器只会用新的样式将元素重绘一次(这就是重绘,或者说重新构造样式)。重排会导致重绘,重绘不一定会导致重排。

重排和重绘这两个操作都是非常昂贵的,因为 JavaScript 引擎线程与 GUI 渲染线程是互斥,它们同时只能一个在工作。

如何减少重绘和重排
1、DOM操作是很耗费性能的操作,尽量减少DOM操作的次数和频率,例如通过JS操作DOM时,将操作集中在一起,避免频繁操作
2、尽量减少table使用,table属性变化使用会直接导致布局重排或者重绘
3、当dom元素position属性为fixed或者absolute, 可以通过css形变触发动画效果,此时是不会触发reflow的
4、不要把 DOM 结点的属性值放在一个循环里当成循环里的变量
5、如果需要创建多个DOM节点,可以使用DocumentFragment创建完后一次性的加入document 6、使用 transform 和 opacity 属性更改来实现动画:在 CSS 中,transforms 和 opacity 这两个属性更改不会触发重排与重绘,它们是可以由合成器(composite)单独处理的属性。

8.2 使用事件委托

事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。所有用到按钮的事件(多数鼠标事件和键盘事件)都适合采用事件委托技术, 使用事件委托可以节省内存。

8.3 垃圾回收

及时清理不再使用的内存,避免内存泄漏,例如在使用完毕后及时解除事件绑定、清空引用等。

8.4 使用更高效的算法

8.5 使用异步操作:对于一些耗时的操作,可以使用异步操作,例如使用异步加载图片、使用异步请求数据等,以提高页面的响应速度

8.6 使用节流防抖

  1. 节流(throttle):指在一定时间间隔内,只执行一次事件,即将一段时间内连续触发的多个事件合并成一个。常用的实现方式是通过设置一个定时器,在一定时间后执行最后一次触发的事件,同时清除定时器,等待下一次事件的触发。例如,当用户在滚动页面时,滚动事件会被不断触发,为了减少事件处理的次数,可以通过节流来控制事件的执行频率。
function throttle(func, wait) {
  let timer = null;
  return function(...args) {
    if (!timer) {
      timer = setTimeout(() => {
        func.apply(this, args);
        timer = null;
      }, wait);
    }
  }
}
  1. 防抖(debounce):指在一定时间间隔内,事件连续触发时,只执行最后一次事件,即等待一段时间后再执行事件。常用的实现方式是通过设置一个定时器,在事件触发后等待一定时间后再执行事件,如果在等待时间内事件再次触发,则清除定时器,重新开始等待,直到等待时间结束后执行最后一次事件。例如,当用户在搜索框中输入文字时,为了减少请求的次数,可以通过防抖来控制请求的发送,等用户输入结束后再发送请求。
function debounce(func, wait) {
  let timer = null;
  return function(...args) {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      func.apply(this, args);
      timer = null;
    }, wait);
  }
}