前端性能与体验优化 - 图1

1 Network

1.1waterfall

waterfall网络请求到资源加载.png
Queueing:将请求资源放入队列,按照优先级顺序依次下载
Stalled:因放入队列时,导致停滞的时间
DNS LookUP: DNS域名解析时间
Initial connection:开始建立网络连接,建立TCP连接的时间,包括TCP的三次握手
SSL:HTTPS加密,浏览器和服务器建立安全连接的时间
waiting TTFB:TTFB 是 Time to First Byte 的缩写,指的是浏览器开始收到服务器响应数据的时间(后台处理时间+重定向时间),是反映服务端响应速度的重要指标。TTFB 时间都在 50 ms 以下,这个时间就是我们优化时候可以追求的时间。TTFB 时间如果超过了 500 ms,用户在打开网页的时候就会感觉到明显的等待。
Content DownLoad :浏览器下载资源的时间

image.png
speed index时间小于4s相对为理想状态

1.3frame帧频

ctrl+shift+p,输入frame可以显示页面刷新频率
image.png
image.png

2 RAIL

R:Response响应,处理事件应在50ms内完成
A:Animation动画, 每10ms产生一帧
I: Idle空闲,尽可能增加空闲时间,一次处理异步时间不应超过50ms
L: Load加载提示,在5s内完成加载并可以交互

3 性能测试工具

1.1 webpagetest

image.png
image.png

Details 详细的细节数据

image.png

Waterfall View

image.png
image.png
重要指标:主线程的空闲时间(Brower Main Thread)和页面可交互时间(Page is Interactive)

1.2本地搭建webpagetest

1.2.1安装 WPT Agent 和 WPT Server

Docker基本环境配置好后,接下来需要安装 WPT 的包了。WPT 的软件包分为 Agent 和 Server 两个部分,对应:

依赖检查
WPT 已经安装好了,WPT的对应配置检查可以通过:http://localhost:4000/install 来看它的依赖是否都安装。
image.png
Mac 下 Traffic Shaping问题
OSX 下会遇到Error configuring traffic-shaping报错,这是因为OSX下还没有实现 traffic-shaping
image.png
可以去掉traffic shaping特性通过在settings/locations.ini设置一个假的connectivity值,
并且在agent运行的时候增加—shaper参数。
更好的办法是基于原有的WPT agent/server镜像制作新的Docker镜像,方便后续搭建Docker集群。

1.2.2创建自己的webpagetest镜像

Server
创建一个server文件夹,包含Dockerfile和locations.ini文件。
Dockerfile:

  1. FROM webpagetest/server
  2. ADD locations.ini /var/www/html/settings/

locations.ini:

  1. [locations]
  2. 1=Test_loc
  3. [Test_loc]
  4. 1=Test
  5. label=Test Location
  6. group=Desktop
  7. [Test]
  8. browser=Chrome,Firefox
  9. label="Test Location"
  10. connectivity=LAN

本地build镜像

  1. $ docker build -t local-wptserver .

Agent
创建一个agent文件夹,包含Dockerfile和script.sh文件。
Dockerfile

  1. FROM webpagetest/agent
  2. ADD script.sh /
  3. ENTRYPOINT /script.sh

script.sh

  1. #!/bin/bash
  2. set -e
  3. if [ -z "$SERVER_URL" ]; then
  4. echo >&2 'SERVER_URL not set'
  5. exit 1
  6. fi
  7. if [ -z "$LOCATION" ]; then
  8. echo >&2 'LOCATION not set'
  9. exit 1
  10. fi
  11. EXTRA_ARGS=""
  12. if [ -n "$NAME" ]; then
  13. EXTRA_ARGS="$EXTRA_ARGS --name $NAME"
  14. fi
  15. python /wptagent/wptagent.py --server $SERVER_URL --location $LOCATION $EXTRA_ARGS --xvfb --dockerized -vvvvv --shaper none

让 script.sh 可执行

  1. chmod u+x script.sh

制作Agent镜像

  1. $ docker build -t local-wptagent .

开始运行一个Webpagetest Docker实例

  1. docker run -d -p 4000:80 local-wptserver
  2. docker run -d -p 4001:80 --network="host" -e "SERVER_URL=http://localhost:4000/work/" -e "LOCATION=Test" local-wptagent

最后访问 http://127.0.0.1:4000 即可查看到效果。
image.png

1.3lighthouse

使用npm本地安装
npm install -g lighthouse
或者在Chrome中直接使用
image.png
会生成一份网站的各项性能报告,可以根据报告对网站进行优化。

1.4 Chrome Devtools

1.4.1 查看文件的size,是否开启压缩

查看文件在网络传输过程,是否开启压缩传输。

1.4.2 performance

根据生成线程执行报告,查看主线程中执行的任务
image.png

关键时间节点 描述 含义
TTFB time to first byte(首字节的时间) 从请求到数据返回第一个字节所消耗的时间
TTI Time to Interactive(可交互时间) DOM树构建完成,可以执行事件
DCL DOMContentLoaded(事件消耗时间) 当HTML文档被完全加载和解析完成后,DOMContentLoaded事件被触发
L onLoad事件耗时 当依赖资源全部加载完毕之后才触发
FP First Paint 首次绘制 第一个像素点绘制到屏幕的时间
FCP First Contentful Paint 首次内容绘制 首次绘制文本,图片,非空白节点的时间
FMP First Meaningful paint首次有意义绘制 首次有意义绘制,页面可用性的度量标准
LCP Largest Contentful Paint最大内容绘制 在viewport中页面最大内容元素加载时间
FID First Input Delay 首次输入延迟 用户首次和页面交互(点击链接或者点击按钮)到页面响应交互的时间

4 WEB API

4.1计算 DOMContentLoaded 时间

  1. window.addEventListener('DOMContentLoaded', (event) => {
  2. let timing = performance.getEntriesByType('navigation')[0];
  3. console.log(timing.domInteractive);
  4. console.log(timing.fetchStart);
  5. let diff = timing.domInteractive - timing.fetchStart;
  6. console.log("TTI: " + diff);
  7. })

4.2观察长任务(performance 中 Task)

  1. const observer = new PerformanceObserver((list) => {
  2. for (const entry of list.getEntries()) {
  3. console.log(entry)
  4. }
  5. })
  6. observer.observe({entryTypes: ['longtask']})

4.3监听视窗激活状态

  1. // 窗口激活状态监听
  2. let vEvent = 'visibilitychange';
  3. if (document.webkitHidden != undefined) {
  4. vEvent = 'webkitvisibilitychange';
  5. }
  6. function visibilityChanged() {
  7. if (document.hidden || document.webkitHidden) {
  8. document.title = '客官,别走啊~'
  9. console.log("Web page is hidden.")
  10. } else {
  11. document.title = '客官,你又回来了呢~'
  12. console.log("Web page is visible.")
  13. }
  14. }
  15. document.addEventListener(vEvent, visibilityChanged, false);

4.4监听网络变化

  1. var connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
  2. var type = connection.effectiveType;
  3. function updateConnectionStatus() {
  4. console.log("Connection type changed from " + type + " to " + connection.effectiveType);
  5. type = connection.effectiveType;
  6. }
  7. connection.addEventListener('change', updateConnectionStatus);

4.5更多计算规则

通过在控制台输入performance.timing,可以查看到左右的时间节点
image.png

  1. DNS 解析耗时: domainLookupEnd - domainLookupStart
  2. TCP 连接耗时: connectEnd - connectStart
  3. SSL 安全连接耗时: connectEnd - secureConnectionStart
  4. 网络请求耗时 (TTFB): responseStart - requestStart
  5. 数据传输耗时 : responseEnd - responseStart
  6. DOM 解析耗时 : domInteractive - responseEnd
  7. 资源加载耗时 : loadEventStart - domContentLoadedEventEnd
  8. First Byte 时间: responseStart - domainLookupStart
  9. 白屏时间: responseEnd - fetchStart
  10. 首次可交互时间 TTI : domInteractive - fetchStart
  11. DOM Ready 时间 DCL : domContentLoadEventEnd - fetchStart
  12. 页面完全加载时间 L: loadEventStart - fetchStart
  13. http 头部大小 transferSize - encodedBodySize
  14. 重定向次数 performance.navigation.redirectCount
  15. 重定向耗时 : redirectEnd - redirectStart

5 浏览器渲染路径

浏览器接收到服务器返回的数据,关键渲染页面经历的过程
image.png

  • JavaScript:脚本及事件处理
  • Style:样式合成计算
  • Layout:布局,再次布局(回流reflow)只关心元素的大小和位置
  • Paint:重绘,颜色、阴影、文字等
  • Composite:合成层

Layout和Paint处理不当会消耗大量时间,占用主线程,造成页面卡顿。
image.pngimage.png

5.1减少Layout和Paint

5.1.1影响Layout的因素

  1. 添加、删除元素
  2. 操作styles
  3. 设置display:none
  4. 设置样式的offsetLeft、offsetTop、scrollTop、scrollLeft、clientWidth、clientHeight等
  5. 移动元素位置
  6. 修改浏览器大小、字体大小

    5.1.2避免layout thrashing

    1. let cards = document.getElementsByClassName("MuiPaper-rounded");
    2. const update = (timestamp) => {
    3. for (let i = 0; i <cards.length; i++) {
    4. let top = cards[i].offsetTop;
    5. cards[i].style.width = ((Math.sin(cards[i].offsetTop + timestamp / 100 + 1) * 500) + 'px')
    6. }
    7. window.requestAnimationFrame(update)
    8. }
    9. update(1000);

    以上代码会造成页面多次layout,动画非常卡顿
    image.png
    避免重绘和回流

  7. 使用transform、opacity、filters开启硬件加速渲染,

  8. 使用虚拟dom框架,react、vue、angular
  9. 动画效果应用到position属性为absolute或fixed的元素(脱离文档流)
  10. 分离读写,使用fastdom库
  11. 使用will-change:transform 提取到单独图层

image.png
读写分离
使用fastDom进行优化,将对 dom 的读和写分离

  1. let cards = document.getElementsByClassName("MuiPaper-rounded");
  2. const update = (timestamp) => {
  3. for (let i = 0; i < cards.length; i++) {
  4. fastdom.measure(() => {
  5. let top = cards[i].offsetTop;
  6. fastdom.mutate(() => {
  7. cards[i].style.width =
  8. Math.sin(top + timestamp / 100 + 1) * 500 + "px";
  9. });
  10. });
  11. }
  12. window.requestAnimationFrame(update)
  13. }
  14. update(1000);

fastdom在线预览:fastdom demo (http://wilsonpage.github.io/fastdom/examples/animation.html)

5.1.3 减少Paint

尽可能通过transform以及opacity设置动画
注意:将需要做动画的层可设置willchange:”transform”,可以创建新的图层。

5.2Composite复合线程

5.3浏览器一帧的生命周期

高频事件处理函数,防抖,一帧内多次触发InputEvents,会造成性能的浪费
Frame生命周期.png

5.4 react优化一帧内完成的事件

image.png

6 资源压缩优化

  • 可以减少http请求次数
  • 减少请求资源的大小

    6.1 代码优化

    6.1.1 html优化

    使用html-minifier工具

    6.1.2 js优化

    webpack中使用插件,uglifyjs-webpack-plugin,支持es6替换terser-webpack-plugin

    6.1.3 css优化

    clean-css压缩工具

    6.2 图片优化

    image.png

    6.2.1 图片格式选择

    压缩jpg:imagemin/imagemin
    压缩png:imagemin/pngquant

    6.2.2 图片懒加载

    verlok/lazyload
    yall.js
    原生的图片懒加载

    6.2.3 响应式、或者渐进式加载图片

    给img标签设置sizes、srcset的属性

    6.3 字体优化

    通过font-face引入字体,并设置font-display的属性
    image.png

    7 webpack工具

    7.1 tree-shaking

    代码要基于es6模块化,才能使用tree-shaking。
    babel的配置文件中,要把modules设置为false,才能使用tree-shaking。
    把mode设置为production,自动开启tree-shaking功能,内部主要使用terser-webpack-plugin插件。

取消对某些文件设置tree-shaking:在package.json中配置sideEffects

7.2 依赖优化

7.2.1 noparse不解析一些库

提高构建速度,大文件库直接使用不打包,如lodash

7.2.2 DLLPlugin动态链接库

把经常使用的一些库,不变的库,减少重复打包,直接使用动态链接库。
通过DllPlugin插件,将一些比较大的,基本很少升级的包拆分出来,生成xx.dll.js文件,通过manifest.json引用。

  1. // webpack.dll.config.js
  2. const path = require("path");
  3. const webpack = require("webpack");
  4. module.exports = {
  5. mode: "production",
  6. entry: {
  7. react: ["react", "react-dom"],
  8. },
  9. output: {
  10. filename: "[name].dll.js",
  11. path: path.resolve(__dirname, "dll"),
  12. library: "[name]"
  13. },
  14. plugins: [
  15. new webpack.DllPlugin({
  16. name: "[name]",
  17. path: path.resolve(__dirname, "dll/[name].manifest.json")
  18. })
  19. ]
  20. };

在script中添加执行

  1. "scripts": {
  2. "dll-build": "NODE_ENV=production webpack --config webpack.dll.config.js",
  3. },

7.2.3 code spliting代码拆分

第一种:多入口拆分
第二种:splitChunks提取公共代码,拆分业务代码和第三方库代码

  1. optimization: {
  2. splitChunks: {
  3. cacheGroups: {
  4. vendor: {
  5. name: 'vendor',
  6. test: /[\\/]node_modules[\\/]/,
  7. minSize: 0,
  8. minChunks: 1,
  9. priority: 10,
  10. chunks: 'initial'
  11. },
  12. common: {
  13. name: 'common',
  14. test: /[\\/]src[\\/]/,
  15. chunks: 'all',
  16. minSize: 0,
  17. minChunks: 2
  18. }
  19. }
  20. }
  21. },

7.3 资源缓存持久化

  • 每次打包后文件有唯一的hash
  • 文件修改后,更新hash值
  • 充分利用浏览器缓存

    7.4 webpack监测与分析工具

  • stats分析与可视化图

  • webpack-bundle-analyzer进行体积分析
  • speed-measure-webpack-plugin速度分析

    01 webpack chart插件

    1. webpack --profile --json > stats.json

    image.png

    02 source-map-explorer

    image.png

    03 bundle-analyzer

    image.png

    8 资源传输过程中优化

    开启Gzip压缩

    通过nginx开启Gzip压缩
    nginx.conf

    1. gzip on;
    2. # 字节多大进行压缩
    3. gzip_min_length 1K;
    4. # 压缩级别官网建议是6
    5. gzip_comp_level 6;
    6. # 对哪些格式文件压缩
    7. gzip_types text/plain application/javascript application/x-javascript text/css application/xml
    8. text/xml text/javascript application/json;
    9. gzip_static on;
    10. # 添加到header中
    11. gzip_vary on;
    12. gzip_buffers 4 16k;
    13. gzip_http_version 1.1;

    开启keep-alive

    可以和服务器的tcp连接进行复用,能够减少连接性能的开销
    在nginx中进行设置

    1. # 超时时间,超过80秒,断开tcp连接
    2. keepalive_timeout 80;
    3. # 建立tcp连接后,可开启100次请求
    4. keepalive_requests 100;

    HTTP缓存

  • Cache-Control(1.1版本)/Expires(1.0版)

  • Etag + If-None-Match
  • Last-Modified(只能精确到秒,不准确) + If-Modified-Since

image.png
image.png
Etag 和 If-None-Match相等,直接拿缓存的资源,如果不相等,重新请求服务器资源。
前端性能与体验优化 - 图31

Service Workers

javaScript 是单线程的,随着web业务的复杂化,开发者逐渐在js中做了许多耗费资源的运算过程,这使得单线程的弊端更加凹显。web worker正是基于此被创造出来,它是脱离在主线程之外的,我们可以将复杂耗费时间的事情交给web worker来做。但是web worker作为一个独立的线程,他的功能应当不仅于此。sw便是在web worker的基础上增加了离线缓存的能力。
image.png

  1. 离线支持访问
  2. 加速重复访问
  3. 独立的 worker 线程,独立于当前网页进程,有自己独立的 worker context;
  4. 能向客户端推送消息;
  5. 不能直接操作 DOM;
  6. 出于安全的考虑,必须在 HTTPS 环境下才能工作;

Vue框架下添加service workers

  1. // serviceWorker.js
  2. import { register } from 'register-service-worker'
  3. if (process.env.NODE_ENV === 'production') {
  4. register('service-worker.js', {
  5. ready () {
  6. console.log(
  7. 'App is being served from cache by a service worker.'
  8. )
  9. },
  10. registered () {
  11. console.log('Service worker has been registered.')
  12. },
  13. cached () {
  14. console.log('Content has been cached for offline use.')
  15. },
  16. updatefound () {
  17. console.log('New content is downloading.')
  18. },
  19. updated () {
  20. console.log('New content is available; please refresh.')
  21. window.location.reload(true) // 这里需要刷新页面
  22. },
  23. offline () {
  24. console.log('No internet connection found. App is running in offline mode.')
  25. },
  26. error (error) {
  27. console.error('Error during service worker registration:', error)
  28. }
  29. })
  30. }

在 webpack.config.js的plugins 加入

  1. plugins: [
  2. new SWPrecacheWebpackPlugin({
  3. cacheId: 'my-project-name',
  4. filename: 'service-worker.js',
  5. staticFileGlobs: ['dist/**/*.{js,html,css}'],
  6. minify: true,
  7. stripPrefix: 'dist/'
  8. }),
  9. new WebpackPwaManifest({
  10. name: 'My Progressive Web App',
  11. short_name: 'MyPWA',
  12. description: 'My awesome Progressive Web App!',
  13. background_color: '#ffffff',
  14. crossorigin: 'use-credentials', //can be null, use-credentials or anonymous
  15. icons: [
  16. {
  17. src: path.resolve('src/assets/icon.png'),
  18. sizes: [96, 128, 192, 256, 384, 512] // multiple sizes
  19. },
  20. {
  21. src: path.resolve('src/assets/large-icon.png'),
  22. size: '1024x1024' // you can also use the specifications pattern
  23. }
  24. ]
  25. }),
  26. // ...
  27. ]

打包出来的代码根目录里面多了个 service-worker.js ,html文件里面 pwa 相关元素也加上了。
在入口 main.js 引入该文件

  1. import './serviceWorker'

http2

  • 二进制分帧
  • 首部压缩
  • 服务器推送
  • 流量控制
  • 多路复用
  • 请求优先级
  • 服务器推送http2_push: ‘xxx.jpg’具体升级方式也很简单,修改一下 nginx 配置

使用http2必须开启https安全认证,需要生成ssl证书

生成ssl证书命令

  1. openssl genrsa -des3 -passout pass:shenshuai -out server.pass.key 2048
  2. openssl rsa -passin pass:shenshuai -in server.pass.key -out server.key
  3. openssl req -new -key server.key -out server.csr
  4. openssl x509 -req -sha256 -days 3650 -in server.csr -signkey server.key -out server.crt

设置了本地正式,谷歌浏览器会禁止访问。此时可以直接在浏览器输入thisisunsafe
开启了http2之后,接口请求的多路复用效果image.png

image.png

nginx开启HTTP2

在nginx.conf中设置

  1. server {
  2. listen 443 ssl http2;
  3. root /var/www/html;
  4. location / {
  5. }
  6. }

nginx开启服务端推送资源

开启主动推送后,会减少TTFB请求的时间

  1. location / {
  2. root /dist
  3. index index.html index.htm;
  4. http2_push /img/img1.jpg;
  5. http2_push /img/img2.jpg;
  6. }

image.png

9资源加载优先级preload和prefetch

preload

可以改变资源加载的优先级顺序

  1. <link rel="prefetch" href="https://fonts.gstatic.com/s/longcang/v5/LYjAdGP8kkgoTec8zkRgqHAtXN-dRp6ohF_hzzTtOcBgYoCKmPpHHEBiM6LIGv3EnKLjtw.113.woff2" as="font"/>

image.png
通过设置preload,可以优先加载需要的资源

prefetch

主线程空闲时,可提前加载后边页面需要用的资源

10 windowing窗口化

  • 加载大列表,大列表的每一行会严重影响性能
  • 即使使用了Lazy loading仍然会让DOM过大
  • windowing只渲染可见的行,渲染和滚动的性能可以大幅提升

    11 骨架组件

    用 css 提前占好位置,当资源加载完成即可填充,减少页面的回流与重绘,同时还能给用户最直接的反馈。
    前端性能与体验优化 - 图37
    react-placeholder react框架实现骨架屏
    vue-skeleton-webpack-plugin 自动生成并自动插入静态骨架屏

12 常见面试题

12.1从输入url到页面显示经历了什么

Chrome 采用多进程架构,其顶层存在一个 Browser process 用以协调浏览器的其它进程。
image.png
Chrome 的主要进程:

  • Browser Process:
    • 负责地址栏书签栏,以及前进后退按钮的工作
    • 处理浏览器的网络请求,文件访问
  • Render Process:负责一个 tab 内关于网页呈现的所有事情
  • Plugin Process:控制网页中用到的所有插件
  • GPU Process:处理GPU图形相关的事情

image.png

Browser Process进程

该进程下包括其他一些进程

  • UI thread: 控制浏览器上的按钮和输入框
  • network thread: 处理网络请求,资源下载等
  • storage thread: 控制文件的访问

UI线程
image.png
网络线程image.png
UI线程和网络线程,都是在浏览器browser process进程中完成、

渲染进程

该进程下包含多个线程,主要有:

  1. GUI thread
  2. Js main thread
  3. 事件处理thread
  4. 定时器事件回调线程
  5. ajax异步网络请求线程

浏览器渲染原理
image.png
之后进入渲染进程,渲染进程中包含有主线程。

image.png
image.png

绘制与合成阶段

image.png

12.2 优化首屏加载

image.png
量化标准
First Contentful Paint (FCP) 首次绘制内容
image.png
Largest Contentful Paint (LCP) 最大内容的绘制
image.png
Time to interactive (TTI) 首次可交互时间
image.png
首屏优化方法:

  1. 解决资源体积过大:资源压缩、传输过程中压缩、代码拆分、Tree shaking、http2、缓存
  2. 首页内容过多:路由、懒加载、预渲染ssr、骨架屏inline css
  3. 加载顺序调整: preload、prefetch

https://mp.weixin.qq.com/s/SL_PDTFKUEQcOC9zJDgsoA