# 前端性能优化 **Repository Path**: alivedog/front-end-Performance-Optimization ## Basic Information - **Project Name**: 前端性能优化 - **Description**: 前端性能优化 掌握行业实用专业前沿的解决方案 - **Primary Language**: JavaScript - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 4 - **Created**: 2021-11-15 - **Last Updated**: 2021-11-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 前端性能优化 ## 介绍 前端性能优化 掌握行业实用专业前沿的解决方案 ## 性能优化的指标和工具 ### Google Chrome network 建议配置 ![Image 'network配置'](image/network配置.png) ### 总体资源概况 ![Image '总体资源概况'](image/总体资源概况.png) 对网站性能影响比较大的指标:TTFB(一个资源从请求发出去到请求回来的时间) ### 保存性能分析结果 ![Image '保存性能分析结果'](image/保存性能分析结果.png) ### 查看页面帧数 ```bash shift + ctrl + p #调出控制台 # 输入框输入 frame # 选择 FPS 选项 ``` ![Iamge '页面帧数指标'](image/页面帧数指标.png) ### 性能优化 #### 加载 1.speed index < 4s 2.TTFB 尽可能小 3.页面加载时间 4.首次渲染 #### 响应 交互动作的反馈时间 帧率 FPS 异步请求的完成时间 ### 可量化测量模型 RAIL Response 响应 处理事件应在 50 ms 以内完成 Animation 动画 每 10ms 产生一帧 Idle 空闲 尽可能增加空闲时间 Load 加载 在 5s 内完成内容加载并可以交互 ### 性能测量工具 Chrome DevTools 开发调试、性能测评 Lighthouse 网站整体质量评估 WebPageTest 多测试地点、全面性能报告 https://webpagetest.org/ #### WebPageTest 本地部署(使用需翻墙) docker 拉取镜像 1.拉取 server 镜像 ```bash docker pull webpagetest/server ``` 2.拉取 agent 镜像 ```bash docker pull webpagetest/agent ``` 3.启动 webpagetest/server 镜像 ```bash docker run -d -p 4000:80 webpagetest/server ``` 4.启动 webpagetest/agent 镜像 ```bash docker run -d -p 4001:80 --network="host" -e "SERVER_URL=http://localhost:4000/work/" -e "LOCATION=Test" webpagetest/agent ``` ### 创建自定义镜像方便以后自定义部署和安装 一、自定义创建 webpagetest/server 镜像 1)创建自定义目录 2)该目录下创建名为 Dockerfile 的文件,添加如下配置: ```bash FROM webpagetest/server ADD locations.ini /var/www/html/settings/ ``` 3)创建 locations.ini 文件,添加如下配置 ```bash [locations] 1=Test_loc [Test_loc] 1=Test label=Test Loction group=Desktop [Test] browser=Chrome,Firefox label="Test Location" connectivity=LAN ``` 4)打包该文件夹 ```bash docker build -t wpt-windows-server . ``` 二、自定义创建 webpagetest/agent 镜像 1)创建自定义目录 2)该目录下创建名为 Dockerfile 的文件,添加如下配置: ```bash FROM webpagetest/agent ADD script.sh / ENTRYPOINT /script.sh ``` 3)创建 script.sh 文件,添加如下配置 ```bash #!/bin/bash set -e if [ -z "$SERVER_URL" ]; then echo >&2 'SERVER_URL not set' exit 1 fi if [ -z "$LOCATION" ]; then echo >&2 '$LOCATION not set' exit 1 fi EXTRA_ARGS="" if [ -n "$NAME" ]; then EXTRA_ARGS="$EXTRA_ARGS --name $NAME" fi python /wptagent/wptagent.py --server $SERVER_URL --location $LOCATION $EXTRA_ARGS --xvfb --dockerized -vvvvv --shaper none ``` 4)打包该文件夹 ```bash docker build -t wpt-windows-agent . ``` 运行新的镜像 ```bash docker run -d -p 4000:80 wpt-windows-server ``` ```bash docker run -d -p 4001:80 --network="host" -e "SERVER_URL=http://localhost:4000/work/" -e "LOCATION=Test" wpt-windows-agent ``` #### Lighthouse 使用 1、安装 ```bash cnpm install lighthouse -g ``` 2、使用并测试 ```bash lighthouse https://www.bilibili.com ``` 3、生成报告地址: ```bash Printer html output written to C:\Users\lyc45\m.bilibili.com_2020-10-07_14-52-21.report.html +75ms ``` 4、查看报告 直接在浏览器地址栏输入 C:\Users\lyc45\m.bilibili.com_2020-10-07_14-52-21.report.html 即可 #### 对资源进行手动阻止加载 chrome shift + ctrl + p show request blocking 点击 Enable request blocking 后面的 "+" 输入 ```bash log*.js: ``` 表示把所有匹配的 js 文件阻止加载 #### node 后台性能优化 使用 compression 对资源进行压缩 ```js const express = require("express"); const app = express(); //... const compression = require("compression"); app.use(compression()); //... ``` #### 使用 chrome 的 performance 进行分析 主线程分析 ![Image '主线程'](image/主线程.png) 在 chrome dev tools 中按 esc 键可进入常用功能菜单 建议常用功能: ![Image '常用功能'](image/常用功能.png) 使用 Chrome DevTools 进行性能测试 Audit(Lighthouse) Throttling 调整网络吞吐 Performance 性能分析 Network 网络加载分析 #### 常用的性能测量 APIs 关键时间节点(Navigation Timing, Resource Timing) 网络状态(Network APIs) 客户端服务端协商(HTTP Client Hints)& 网页显示状态(UI APIs) 重要的时间点如何计算: DNS 解析耗时: domainLookupEnd - domainLookupStart TCP 连接耗时: connectEnd - connectStart SSL 安全连接耗时: connectEnd - secureConnectionStart 网络请求耗时 (TTFB): responseStart - requestStart 数据传输耗时: responseEnd - responseStart DOM 解析耗时: domInteractive - responseEnd 资源加载耗时: loadEventStart - domContentLoadedEventEnd First Byte 时间: responseStart - domainLookupStart 白屏时间: responseEnd - fetchStart 首次可交互时间: domInteractive - fetchStart DOM Ready 时间: domContentLoadEventEnd - fetchStart 页面完全加载时间: loadEventStart - fetchStart http 头部大小: transferSize - encodedBodySize 重定向次数:performance.navigation.redirectCount 重定向耗时: redirectEnd - redirectStart 前端可以把这些重要的时间节点计算后发送给后台 ```js // load 事件后触发 window.addEventListener("load", (event) => { // Time to Interacrtive 可交互时间 let timing = performance.getEntriesByType("navigation")[0]; // 计算 tti = timing.domInteractive - timing.fetchStart; let tti = timing.domInteractive - timing.fetchStart; console.log("TTI: " + tti); }); ``` 前端获取长任务信息 ```js // 通过 PerformanceObserver 得到所有的 long tasks 对象 let observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log(entry); } }); // 监听long tasks observer.observe({ entryTypes: ["longtask"] }); ``` 了解客户还是不是在看当前页面 ```js let vEvent = "visibilitychange"; // webkit 浏览器 if (document.webkitHidden != undefined) { // webkit 事件名称 vEvent = "webkitvisibilitychange"; } function visibilitychanged() { if (document.hidden || document.webkitHidden) { // 页面不可见 console.log("Web page is hidden"); } else { // 页面可见 console.log("Web page is visibile"); } } document.addEventListener(vEvent, visibilitychanged, false); ``` 获取客户端网络状态 ```js let connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; // 网络连接状态 let type = connection.effectiveType; // 网络状态发生变化触发的函数 function updateConnectionStatus(){ console.log("connection type change from " + type + "to " + connection.effectiveType); } // 添加网络监听,一旦网络状态发生变化就触发 updateConnectionStatus 方法 connection.addEventListener('change', updateConnectionStatus) ``` ## 渲染优化 ### 浏览器的渲染流程 ![Image '浏览器的渲染流程'](image/浏览器的渲染流程.png) ### 回流 对于浏览器的二次绘制成为回流(重绘) 会导致重绘的操作: ![Image '影响回流的操作'](image/影响回流的操作.png) #### 查看页面中是否有回流 页面上代码: ```js // 获取页面上的卡片 let cards = document.getElementsByClassName("MuiCardMedia-root"); const update = () => { cards[0].style.width = "800px"; }; ``` 浏览器中识别有没有回流发生 ![Image '浏览器中识别有没有回流发生1'](image/浏览器中识别有没有回流发生1.png) ![Image '浏览器中识别有没有回流发生2'](image/浏览器中识别有没有回流发生2.png) #### 避免回流 元素的位置属性的读(强制对布局进行重新计算)与写都会导致回流,连续不断的强制回流会导致布局抖动。 ```js // 布局抖动示例 // 获取所有页面上的卡片元素 let cards = document.getElementsByClassName("MuiCardMedia-root"); // 一个连续触发的更新卡片图片宽度的方法 const update = (timestamp) => { for (let i = 0; i < cards.length; i++) { // 获取 offsetTop,设置新的 width cards[i].style.width = (Math.sin(cards[i].offsetTop + timestamp / 1000) + 1) * 500 + "px"; } window.requestAnimationFrame(update); }; window.addEventListener("load", update); ``` 避免回流一个比较重要的方法就是在需要改变元素位置的时候不要使用 left,right,top,bottom 这样的属性; 而是使用 transform translate 这样的属性,这种属性不会导致回流,只是会影响复合的过程。 第二个就是使用虚拟 dom 与原 dom 进行比对,然后做出修改,把批量的修改进行统一处理,避免多次回流。 第三个是读写分离:offset(读) 与设置 css 位置属性(写)分开。进行批量的读和批量的写分开。 #### FastDom: 布局抖动解决方案 FastDom 两个比较重要的 API: fastdom.measure(读操作)、fastdom.mutate(写操作)。 对上面的代码进行修改: ```js // 使用 fastdom 防止布局抖动 // ... for (let i = 0; i < cards.length; i++) { fastdom.measure(() => { // 读取 offsetTop 值 let top = cards[i].offsetTop; }); // 写 fastdom.mutate(() => { // 获取 offsetTop,设置新的 width cards[i].style.width = (Math.sin(top + timestamp / 1000) + 1) * 500 + "px"; }); } // ... ``` #### 复合线程(compositor thread)与图层(layers) 查看页面图层: chrome devtools: shift + ctrl + p -> 搜索 layers ->(show layers) 图层页面: ![Image '图层工具'](image/图层工具.png) #### 只触发复合而不触发重绘和重排 以下 4 个属性只触发复合 ```css { /* 修改位置 */ Position transform: translste(npx, npx); /* 修改大小 */ Scale transform: scale(n); Rotation transform: rotate(ndeg); Opacity opacity: 0...1; } ``` 复合线程做了什么 将页面拆分图层进行绘制再进行复合 利用 devTools 了解网页的图层拆分情况 哪些样式仅影响复合 使用 translate 属性避免重排和重绘 ```css { keyframes rotate { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } } ``` 性能分析查看: ![Image '性能监测'](image/性能监测.png) #### 使用浏览器捕获重绘区域 shift + ctrl + p 搜索 Rendering 选择 show Rendering ![Iamge '捕获重绘区域'](image/捕获重绘区域.png) 在浏览器上使用 will-change 属性告知浏览器该元素会有哪些变化的方法,这样浏览器可以在元素属性真正发生变化之前提前做好对应的优化准备工作。 这种优化可以将一部分复杂的计算工作提前准备好,使页面的反应更为快速灵敏。 用好这个属性并不是很容易: 不要将 will-change 应用到太多元素上:浏览器已经尽力尝试去优化一切可以优化的东西了。有一些更强力的优化,如果与 will-change 结合在一起的话,有可能会消耗很多机器资源,如果过度使用的话,可能导致页面响应缓慢或者消耗非常多的资源。 有节制地使用:通常,当元素恢复到初始状态时,浏览器会丢弃掉之前做的优化工作。但是如果直接在样式表中显式声明了 will-change 属性,则表示目标元素可能会经常变化,浏览器会将优化工作保存得比之前更久。所以最佳实践是当元素变化之前和之后通过脚本来切换 will-change 的值。 不要过早应用 will-change 优化:如果你的页面在性能方面没什么问题,则不要添加 will-change 属性来榨取一丁点的速度。 will-change 的设计初衷是作为最后的优化手段,用来尝试解决现有的性能问题。它不应该被用来预防性能问题。过度使用 will-change 会导致大量的内存占用,并会导致更复杂的渲染过程,因为浏览器会试图准备可能存在的变化过程。这会导致更严重的性能问题。 给它足够的工作时间:这个属性是用来让页面开发者告知浏览器哪些属性可能会变化的。然后浏览器可以选择在变化发生前提前去做一些优化工作。所以给浏览器一点时间去真正做这些优化工作是非常重要的。使用时需要尝试去找到一些方法提前一定时间获知元素可能发生的变化,然后为它加上 will-change 属性。 ```css root { will-change: "transform"; } ``` 添加该属性可以让浏览器把可能会发生变化的 transform 元素抽离到单独图层。 #### 减少重绘的方案 利用 Devtools 识别 paint 的瓶颈 利用 will-change 创建新的图层 #### 高频 事件处理函数 防抖 ![Image '帧的生命周期'](image/帧的生命周期.png) #### 使用 rAF 函数优化高频事件处理函数 未使用 rAF 优化处理高频事件 ```js // 侦听鼠标移动事件 // 利用 chrome devtools 可以复现抖动的问题(pointer Events) changeWidth(pos){ //... } window.addEventListener('pointermove', (e)=>{ let pos = e.clientX; changeWidth(pos) }) ``` 使用 rAF 做优化: ```js let ticking = false; window.addEventListener("pointermove", (e) => { let pos = e.clientX; if (ticking) return; ticking = true; window.requestAnimationFrame(() => { changeWidth(pos); ticking = false; }); }); ``` #### React 时间调度实现 requestIdleCallback:如果一帧(16ms)还有空余时间,可以做其他的事情的回调函数,但不同浏览器的实现不好(兼容性不好)。 react 内部通过 rAF 模拟了 rIC ![Image 'rIC在什么时候调用'](image/rIC在什么时候调用.png) #### JS 的开销和如何缩短解析时间 同样大小的 JS 文件和 JPEG 文件开销对比: ![Image '同样大小的JS文件和JPEG文件开销对比'](image/同样大小的JS文件和JPEG文件开销对比.png) 解决方案: Code splitting 代码拆分。按需加载 Tree shaking 代码减重 减少主线程工作量: 避免长任务 避免超过 1kb 的行间脚本(因为浏览器引擎在处理行间脚本时无法做出合理的优化) 使用 rAF 和 rIc 进行时间调度 ![Image '加载时间轴'](image/加载时间轴.png) #### V8 编译原理 ![Image 'v8编译原理'](image/v8编译原理.png) 当 v8 把代码优化后如果发现优化的不对还会进行返优化(撤销已有的优化)这样会导致性能下降,举例: 保存文件,在 Node 环境下运行 ```js const { performance, PerformanceObserver } = require("perf_hooks"); const add = (a, b) => a + b; const num1 = 1; const num2 = 2; performance.mark("start"); for (let i = 0; i < 10000000; i++) { add(num1, num2); } add(num1, "s"); // 因为上面的 add(num1, num2) 和下面的 add(num1, num2) 执行结果是一样的,v8 引擎会自动对代码进行优化,但这段代码 add(num1, "s") 出现了字符串 's' 会导致浏览器自动优化有误,而进行反优化,导致性能下降。 for (let i = 0; i < 10000000; i++) { add(num1, num2); } performance.mark("end"); const observer = new PerformanceObserver((list) => { console.log(list.getEntries()[0]); }); observer.observe({ entryTypes: ["measure"] }); performance.measure("测量1", "start", "end"); ``` 抽象语法树: 源码 => 抽象语法树 => 字节码 Bytecode => 机器码 编译过程会进行优化 运行时可能发生反优化 #### V8 优化机制 脚本流:当下载的脚本大于 30kb 时会单独开一个线程,给这段代码先做解析,然后边下载边解析,最后把所有解析的结果进行合并。 字节码缓存:源码翻译成字节码之后把可能会重复用到的代码进行缓存。 懒解析:对函数的声明不一定会直接用,所以遇到函数时先不解析,等到用的时候再解析。 有的时候不希望浏览器进行懒解析,希望立即解析,如何做: ```js // 懒解析 let add = (a, b) => a + b; // 饥饿加载(立即加载) // let add = ((a, b) => a + b); ``` 当使用 webpack4 以下的版本打包时会自动把 let add = ((a, b) => a + b); 两边的括号去掉,需要使用 Optimize.js 优化初次加载时间。 #### 对象优化可以做哪些 1、以相同顺序初始化对象成员,避免隐藏类的调整; 2、实例化后避免添加新属性; 3、尽量使用 Array 代替 array-like; ```js // 类数组想调用 forEach 方法: Array.prototype.forEach.call(arrObj, (value, index) => { // 不如在真实数组上效率高 console.log(`${index}:${value}`); }); // 优化: const arr = Array.prototype.slice.call(arrObj, 0); arr.forEach((value, index) => { console.log(`${index}:${value}`); }); ``` 4、避免读取超过数组的长度; 5、避免元素类型转换。 #### HTML 优化 1、减小 iframes s 使用; 2、压缩空白符; 3、避免节点深层级嵌套; 4、删除元素默认属性; 5、避免 table 布局; 6、删除注释; 7、css & js 尽量外链. #### CSS 性能优化 1、减低 CSS 对渲染的阻塞:把 css 文件进行拆分,先加载首次需要用到的,对暂时不需要的放到后面进行加载; 2、利用 GPU 对动画进行完成:多图层处理; 3、使用 contain 属性: ```css { /* 告诉浏览器盒子内的变化不会影响到盒子外面,盒子外的变化也不会影响到盒子里面 */ contain: layout; } ``` 4、使用 font-display 属性:让文字更早的显示到页面上。 ### 资源优化 减少 http 请求数量 减少请求资源的大小 #### 图片资源优化 jpg/jpeg 特点:压缩率高,色彩丰富,适合展示物品,不适合对边缘质量要求高的图片。 图片压缩工具: github.com/imagemin/imagemin png 特点:对 jpg/jpeg 的缺点进行了弥补,但是体积较大。 压缩工具: github.com/imagemin/imagemin-pngquant webP 格式 #### 图片的懒加载 使用 img 原生的懒加载 ```html ``` 第三方图片懒加载方案: verlok/lazyload yall.js Blazy react 插件地址: github.com/Aljullu/react-lazy-load-image-component 基本配置: ```html ``` #### 渐进式图片解决方案 1、progressive-image 2、ImageMagick 3、libjpeg 4、jpegtran 5、jpeg-recompress 6、imagemin #### 响应式图片解决方案 ```html img 图片占 50% 的视窗宽度,当 img 宽度为100px、200px、400px、800px、1000px、1400px、1800px 时分别对应相应的图片地址 ``` #### 字体优化 使用 font-display ![Image 'font-display'](image/font-display.png) 使用方法: ```css @font-face { /* ... */ font-display: block; } ``` ### 构建优化 webpack 默认配置: https://webpack.js.org/configuration/mode/#mode-production #### Tree-shaking 上下文未用到的代码(dead code) 基于 ES6 import export package.json 中配置 sideEffects,处理可能会影响全局而无法通过 export 体现的文件 注意 Babel 默认配置的影响 ```js { presets: [ [ "@babel/preset-env", { // preset-env 会把 es6 的模块化语法改为其他的模块化语法,但 tree-shaking 是基于 es6 的模块化语法的,所以要改为 false,告诉 babel 不要转换 modules: false, targets:{ // 对市场份额超过 0.25% 的浏览器都做兼容 browsers: ['>0.25%'] }, // 对不需要的 polyfill 不打包处理 useBuiltIns: "usage", }, ], ], plugins:[ // 配置辅助函数的按需引入 '@babel/plugin-transform-runtime' ], module:{ // 告诉 babel 不需要打包的库 noParse: /lodash/ } } ``` #### 使用 DllPlugin 加快打包速度 避免打包时对不变的库重复构建,而是生成 dll 动态链接库,提高构建速度。 使用方法(对 react、react-dom 生成动态链接库): webpack.dll.config.js: ```js const path = require("path"); const webpack = require("webpack"); module.exports = { mode: "production", entry: { // 设置自己希望要创建的文件名叫 'react' // 希望要创建的动态链接库包含的类为 react 和 react-dom react: ["react", "react-dom"], }, outpot: { // 输出的名字为在入口定义的名字(这里就是 react) filename: "[name].dll.js", // 存放路径 path: path.resolve(__dirname, "dll"), // 库的名称为在入口定义的名字(这里就是 react) library: "[name]", }, plugins:[ // 通过 webpack.DllPlugin 插件生成 dll 文件的描述文件 new webpack.DllPlugin(options:{ // name 与上面的 library: "[name]" 的 name 保持一致 name: '[name]', // 生成描述文件的路径 path: path.resolve(__dirname, 'dll/[name].manifest.json') }) ] }; ``` 然后在 node 环境下运行这个脚本 然后在 webpack.config.js 中做如下配置: ```js plugins:[ new DllReferencePlugin(options:{ manifest: require(`${__dirname}/dll/react.manifest.json)`) }) ], ``` #### webpack 代码拆分的方法 方法 1、手工定义入口,但这样要自己维护,不方便; 方法 2、使用 splitChunks 提取公有代码,拆分业务代码与第三方库; webpack.cinfig.js 配置: ```js { optimization:{ splitChunks:{ // splitChunks 配置 cacheGroups:{ // 进行分组 vendor:{ // 第三方库的配置 name: 'vendor', // 匹配的目录(因为是第三方库,所以要匹配 node_modules) test: /[\\/]node_modules[\\/]/, // 最小大小,默认为 30kb minSize: 0, // 最少拆成多少段 minChunks: 1, // 优先级 priority: 10, chunks: 'initial' }, common:{ // 提取公共的代码 name: 'common', test: /[\\/]src[\\/]/, chunks: 'all', minSize: 0, minChunks: 2 } } } } } ``` #### 同步加载库与异步加载库 ```js // 同步加载 import { add } from "./math"; console.log(add(16, 26)); // 异步加载 import("./math").then((math) => { console.log(math.add(16, 26)); }); ``` #### React 中组件动态引入与 suspense ```js const Card = lazy(factory:()=>import('./Card')) // ... cards.push(model.map(panel =>{ Loading...}> })) ``` #### webpack 的资源压缩 Terser 压缩 JS mini-css-extract-plugin 压缩 CSS 安装插件: ```bash # 提取 css cnpm install mini-css-extract-plugin -D # 压缩 css cnpm install optimize-css-assets-webpack-plugin -D ``` HtmlWebpackPlugin-minify 压缩 HTML #### webpack 持久化缓存方案 每个打包的资源文件有唯一的 hash 值(推荐使用 contentHash) 修改后只有受影响的文件 hash 变化 充分利用浏览器缓存 #### 监测与分析 1、Stats 分析与可视化图 1)使用 Webpack Chart 进行总体状况分析: 工具地址: https://alexkuz.github.io/webpack-chart/ github 地址: https://github.com/alexkuz/webpack-chart 2)使用 source-map-explorer 进行详细分析: ```bash cnpm isntall source-map-explorer -D ``` package.json 配置: ```js { //... scripts: { analyze: "source-map-explorer 'build/*.js'"; } //... } ``` 这个插件是基于 source-map 文件进行分析的,所以要开启 source-map: webpack.config.js 文件: ```js module.exports = { // ... devtool: "hidden-source-map", }; ``` 分析: ```bash npm run analyze ``` 2、webpack-bundle-analyzer 进行体积分析 3、speed-measure-webpack-plugin 速度分析 ```bash cnpm install speed-measure-webpack-plugin -D ``` webpack.config.js 文件: ```js const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); const smp = new SpeedMeasurePlugin(); module.exports = smp.wrap({ // ... }); ``` ```bash npm run build ``` #### React 按需加载 1、React router 基于 webpack 动态引入 2、使用 Reloadable 高级组件 ### 传输加载优化 #### Nginx 相关配置 nginx 基本操作: ```bash # 在 nginx 目录下: # 1 启动 start nginx # 2 重载 当修改配置文件后使用 nginx.exe -s reload # 3 stop快速停止,并不保存相关信息 quit有序停止,会保存相关信息 nginx.exe -s stop|quit # 4 查看版本 nginx.exe -v ``` nginx 配置 gzip: 配置文件路径:/nginx-1.19.2/conf/nginx.conf ```bash # ... http{ # ... # gzip相关配置 # 开启 gzip gzip on; # 超过多大的文件才进行压缩 gzip_min_length 1k; # 压缩级别(1-9) gzip_comp_level 6; # 对已经压缩的静态资源进行直接利用 gzip_static on; # 压缩的类型 gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/xml text/javascript application/json; # 在相应的头部添加 vary 属性,告诉客户端是否已启用 gzip 压缩 gzip_vary on; # 优化压缩过程 gzip_buffers 4 16k; # 压缩时使用的 http 版本 gzip_http_version 1.1; } ``` #### 启用 Keep-Alive 对 TCP 连接进行复用,当浏览器和服务器建立一次 TCP 连接后,接下来的所有请求就都不需要再重复建立连接了。 HTTP 1.1 开始默认是开启的。 一般情况下 keepalive 是为了页面首次加载时的所有资源请求做 TCP 连接优化。 Nginx 的 keepalive 配置: ```bash # 当建立 TCP 连接后如果 65 秒没有再次使用,服务器会关闭 TCP 连接 keepalive_timeout 65; # 一个 TCP 连接被复用的最多次数,当发送第 101 个请求时会开一个新的 TCP 连接,如果首屏加载资源数量超过 100 可以适当放大该参数 keepalive_requests 100; ``` #### HTTP 缓存 1、Cache-Control/Expires 2、Last-Modified + If-Modified-Since(因为与时间有关,所以不推荐) 3、Etag + If-None-Match 与 Last-Modified + If-Modified-Since 类似。但是是基于文件唯一性的判断,所以推荐这个。etag 为文件的特殊标识,不同的文件 etag 不同。 Nginx 的 HTTP 缓存配置: ```bash location / { # ... if ($request_fiename ~* .*\.(?:htm|html)$) { # 现在的主流框架都是单页面应用,都是通过 html 进行后续加载的,所以对 html 文件不能进行缓存,而且一般 html 文件也不大,所以用户每次请求 html 文件都要获取最新的。 # no-cache 表示浏览器不进行缓存; # must-revalidate 表示获取完还要进行验证。 add_header Cache-Control "no-cache, must-revalidate"; # 后面两个配置和上一条意思一样,只是为了考虑兼容性,兼容 HTTP 1.0 的版本,HTTP 1.0 的标准里 Cache-Control 并没有很好的实现。 add_header "Pragma" "no-cache"; # Expires 为 0 表示立即过期 add_header "Expires" "0"; } if ($request_filename ~* .*\.(?:js|css)$) { # js、css 文件缓存 7天 expires 7d; } if ($request_filename ~* .*\.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm)$){ # 图片视频文件缓存 7天 expires 7d } } ``` ### HTTP 头部信息 开发者网站: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers #### Service Workers 作用 1、加速重复访问 2、离线支持 Service Workers 注意 1、延长了首屏时间,但页面总加载时间减少了 2、兼容性,目前兼容了市场上 93.7% 的浏览器,但 IE 兼容的不好 3、只能在 localhost 或 https 下使用 service worker 的使用: 如果是使用了 create react-app 脚手架创建的 react 项目,直接自带 service worker 功能 service worker 需要注册 -> 安装 -> 激活 webpack 插件:WorkboxWebpackPlugin.GenerateSW() ManifestPlugin():生成 asset-manifest.json 文件 在 index.jsx 文件中注册: ```js // ... serviceWorker.register(); ``` 在打包完的 build 目录下会生成 asset-manifest.json 的文件,这个文件定义了哪些文件需要进行缓存,以及缓存资源的文件名。 相关的版本信息会存在同目录下的 precache-manifest.js 的文件下。 service worker 原理: ![Image 'service_worker'](image/service_worker.png) #### HTTP 2 升级 HTTP 2 必须使用 HTTPS 协议,适合较高的请求量的业务需求能发挥明显优势,HTTP 2 优势: 1、二进制传输 2、请求响应多路复用 3、Server push(服务端推送) #### 生成自签名 SSL 证书 ```bash # 生成 server.pass.key 私钥 openssl genrsa -des3 -passout pass:x -out server.pass.key 2048 # 使用上一步生成的 server.pass.key 生成 server.key (这是真正的私钥) openssl rsa -passin pass:x -in server.pass.key -out server.key # 生成 csr 这里会问一些问题 openssl req -new -key server.key -out server.csr # 通过 csr 和 私钥生成自签名证书 # 生成的 server.crt 为证书 server.key 为私钥 openssl x509 -req -sha256 -days 3650 -in server.csr -signkey server.key -out server.crt ``` HTTP 1.1 与 HTTP 2 网络请求比较 ![Image 'HTTP 1.1 与 HTTP 2 网络请求比较'](image/http1.1与http2比较.png) #### Nginx 配置服务端推送 ```bash location / { # ... http2_push /img/me0.jpg; http2_push /img/me1.jpg; http2_push /img/me2.jpg; } ``` 服务器可以在不需要浏览器请求的情况下把上面三张图片推送给浏览器,然后浏览器会存到缓存里,等到需要时使用时再使用,这可以消除 TTFB #### 服务端渲染(SSR) 服务端渲染的好处 1、加速首屏加载 2、更好的 SEO 客户端渲染 vs 服务端渲染: ![Image 'CSR vs SSR'](image/CSR_vs_SSR.png) #### 是否使用 SSR 1、当项目为大型架构,并且包含很多动态页面,需要后台查询数据库,并且页面需要重新组织、渲染时,不如后台把 html 页面生成好直接返给前端,面向公众用户时; 2、搜索引擎排名变得很重要时。 #### IconFont 对比 PNG 的好处 1、多个图标:一套字体,减少获取是的请求数量和体积; 2、矢量图形,可伸缩; 3、直接通过 CSS 修改样式(颜色,大小等)。 #### SVG 的使用 安装 ```bash cnpm install @svgr/webpack -D ``` 在 webpack.config.js 中配置: ```js // ... module: { rules: [ { test: /\.svg$/, use: ["@svgr/webpack"], }, ]; } ``` 页面上: ```vue ``` #### SVG 对比 IconFont 的好处 1、保持了图片能力,支持多色彩; 2、独立的矢量图形,不需要下载整套字体,可以按需引入; 3、XML 语法,便于搜索引擎 SEO 和无障碍读屏软件读取。 #### 使用 flex 做响应式布局 ```html
Header

main

Footer
``` #### 优化资源加载的顺序 浏览器有默认安排资源加载的优先级 可以使用 preload、prefetch 调整优先级 使用: ```html
``` #### preload 和 prefetch 的适用场景 1、preload:提前加载较晚出现,但对当前页面非常重要的资源(比如字体文件) 给字体文件设置 preload: ```html ``` 2、prefetch:当浏览器空闲时预先加载后面可能会用到的资源 使用: ```html ``` #### 图片的动态预加载(使用代码控制预加载) ```js function loadResource(url) { let link = document.createElement("link"); link.rel = "preload"; link.href = url; link.as = "image"; document.head.appendChild(link); } let url = "img/product1.svg"; // 图片预加载 loadResource(url); ``` webpack 打包时的 prefetch 的使用: 在业务文件 js 内: ```js import(/* webpackPrefetch: true*/ "LoginModal"); ``` #### 预渲染页面 预渲染页面技术相当于一个静态的 SSR 技术,在打包的时候就生成了 html 野蛮,而不是请求服务器时再动态计算 html 页面,然后再返回。 比较成熟的插件:react-snap 安装: ```bash cnpm install react-snap -D ``` package.json 文件配置: ```js // ... "scripts":{ // postbuild 为一个钩子函数,执行完 build 后会自动执行 postbuild "postbuild": "react-snap" } ``` 在 index.jsx 中: ```js let root = document.getElementById((elementId: "main")); if (root.hasChildNodes()) { // 已经做了页面预渲染处理 ReactDOM.hydrate(, root); } else { // 页面没做预渲染处理 ReactDOM.render(, root); } ``` 内联样式,避免明显的 FOUC(样式闪动): 在 package.json 文件里: ```js "reactSnap":{ "inlineCss": true } ``` #### 预渲染的作用 1、大型单页应用的性能瓶颈:JS 下载 + 解析 + 执行; 2、SSR 的主要问题:牺牲 TTFB 来补救 First Paint;实现复杂; 3、Pre-rendering 打包时提前渲染页面,没有服务端参与。 #### Windowing(窗口化)提高列表性能 ![Image '什么是windowing'](image/什么是windowing.png) 安装 react-window 插件: ```bash cnpm install react-window -D ``` #### windowing 的作用 1、加载大列表、大表单的每一行严重影响性能; 2、Lazy loading 仍然会让 DOM 变得过大; 3、windowing 只渲染可见的行,渲染和滚动的性能都会提升。 使用: ListComponents.js: ```js // 这里举例引入的是静态的表单组件,还有一个动态的表单组件这里不做演示 import { FixedSizeList } from "react-window"; // model 文件: // [{ // name: '...', // image: '...', // body: '...' // }, // { // name: '...', // image: '...', // body: '...' // } // ... // ] import model from "./model"; const items = []; for (let i = 0; i < 100; i++) { items.push(model.map(m=>( {m.name} ))) } const Row = (index, style) => { let styleExt = { ...style, borderBottom: "1px solid #fff", display: "flex", alignItems: "center", justifyContent: "center", }; return (
{items[index]}
); }; const ListComponent = () => ( {Row} ; ); export default ListComponent ``` 使用: ```jsx ``` #### 使用骨架组件减少布局移动(Layout Shift) 插件安装: ```bash cnpm install react-placeholder -D ``` ### 性能优化问题面试指南 #### web 加载 & 渲染基本原理 从输入 URL 到页面加载显示完成都发生了什么? 解答: 搜索引擎搜索的还是直接请求站点?如果是直接请求站点: 注意:thread 为线程,process 为进程 线程与进程参考: https://www.jianshu.com/p/c1808d0c1d45 1、UI thread 线程对 URL 进行解析: ![Image '搜索引擎or请求站点'](image/搜索引擎or请求站点.png) 2、UI thread 线程解析好后会通知 Network thread 线程: ![Image 'network线程'](image/network线程.png) 3、Render process(渲染进程) ![Image '浏览器进程与渲染进程'](image/浏览器进程与渲染进程.png) ![Image '渲染进程'](image/渲染进程.png) display: none 的元素在 DOM 树上是有的,但在布局树上没有 #### 首屏加载优化 首屏:用户加载体验的 3 个关键时刻既指标参数: ![Image '首屏指标参数'](image/首屏指标参数.png) 关键问题: ![Image '首屏加载关键问题'](image/首屏加载关键问题.png) #### javascript 内存管理 JS 是怎样管理内存的?什么情况会造成内存泄漏? 1、内存释放的主要问题是如何确定不再需要使用的内存。 2、所有的 GC(garbage collection 垃圾回收)都是近似实现的,只能通过判断变量是否还能再次访问到。 1)局部变量,函数执行完,没有闭包引用,就会被标记回收。 2)全局变量,直至浏览器卸载页面时释放。 如何避免: 1、避免意外的全局变量产生; 2、避免反复运行引发大量闭包; 3、避免脱离的 DOM 元素