前端常用性能优化方向

文章整理于 20 年写的 性能优化文章,现添加补充以及详细说明;

用于面试、也可警惕自己日常开发都是个不错的选择;

一、 vue 方向

v-if 和 v-showv-if 会导致重绘重排, 从 DOM 树中删除、成本很大;适应于初始渲染后续不在变化的 DOM;v-show 控制的是 DOM 样式, 避免了重绘重排, 适用频繁切换显示、隐藏 的 DOM;v-for 中一定要用 keyv-for 渲染列表时,遵循就地复用策略;他会根据 key 值去判断某个值是否修改,如果修改,则重新渲染这一项,否则复用之前的元素;不要使用 index 索引作为 key, index 会改变;尽量使用数据唯一值;v-for 和 v-if 不要一起使用v-for 的优先级高于 v-if ; 即先执行完 v-for 才会执行 v-if 逻辑, 会增加 DOM渲染难度;用 template 包一层代替;computed 计算属性代替 watchcomputed 计算属性的值有缓存,只有它依赖的属性值发生改变,才会触发获取逻辑;路由懒加载 以及 KeepAlive避免进入首页就加载全部的前端资源造成白屏时间过长;静态页面(引导页、协议、介绍等页面)可设置 KeepAlive 包裹,缓存起来;事件的销毁 - 释放内存addEventListener 要有对应的 removeEventListener 销毁setTimeout\setInterval 要有对应的 clearTimeout\clearIntervalvue 长列表性能优化Vue 会对数据进行劫持,实现双向数据绑定,但有的时候就说纯粹的数据展示,所以应避免vue静态数据进行劫持, Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了图片懒加载 以及 图片预加载的用时才开始加载,不用不加载;图片懒加载适用于 大批量图片展示的场景;提前加载下次所需图片,图片预加载能够使得用户在浏览时不会出现图片加载一半导致浏览不流畅的情况;插件的按需引入以及取舍(你是否真的需要)比如 UI 库, 往往都是比较大的,很占资源,应当使用按需引入;比如工具类 loadsh, 往往你只是需要一个 防抖、节流,这时自己写一个也很简单,就没必要引入了;服务端渲染 和 预渲染服务端渲染 SSR (vue-server-renderer) 由服务端帮你渲染完成直接返回给浏览器,提高了用户体验,能快速的浏览的所需页面、SEO 友好;减轻了浏览器压力,但相应的造成了服务器的压力;预渲染 (prerender-spa-plugin) 利用了 Puppeteer 的爬取页面的功能,在 Webpack 构建阶段的最后,会本地启动一个 Puppeteer 的服务,访问配置了预渲染的路由,然后将 Puppeteer 中渲染的页面输出到 HTML 文件中,并建立路由对应的目录;

二、 react 方向

性能主要耗费在于update阶段的diff算法,因此性能优化也主要针对diff算法

减少diff算法触发次数(实际上减少update流程的次数);

注:父组件的render必然会触发子组件进入update阶段(无论props是否更新)。

合并setStatesetState 机制是批更新策略,已经降低了update过程的触发次数;尽量无论数据处理多么复杂,保证最后只调用一次setState, 合并 setState 的调用;memomemo会对state和prop进行浅比较控制是否刷新;memo会缓存组件本身,站在全局的角度进行优化, 类似PureComponent、shouldComponentUpdate适当使用 useCallbackuseCallback缓存的是一个函数,是对一个单独的props值进行缓存, 返回上一次的函数引用, 可以保证依赖的值未发生改变的时候,不触发函数引用的改变;在向子组件传递函数props时,每次 render 都会创建新函数,导致子组件不必要的渲染;useCallback 可以保证,无论 render 多少次,我们的函数都是同一个函数,减小不断创建的开销;但是给所有钩子都用 useCallback包裹, 是不对的,因为多数情况下无效,还导致代码可读性变差;需结合 memo 配套使用;useMemouseMemo 缓存的是一个值,可以保证依赖的值未发生改变的时候,不触发值改变;会根据依赖的值计算出结果,当依赖的值未发生改变的时候,不触发状态改变;会在渲染的时候执行, 不是渲染之后执行, 不建议有副作用相关的逻辑;类组PureComponent / shouldComponentUpdateshouldComponentUpdate 的返回值用于判断 React 组件的输出是否受当前 state 或 props 更改的影响,当 props 或 state 发生变化时,shouldComponentUpdate 会在渲染执行之前被调用;PureComponent / shouldComponentUpdate;会进行props和state的浅比较来判断组件是否需要更新( 浅比较:只会比较到两个对象的 ownProperty 是否符合 Object.is() ,不会递归地去深层次比较);**慎用 forceUpdate **forceUpdate 会强制更新页面,直接进入componentWillUpdate阶段,且无法拦截, 跳过优化手段(shouldComponentUpdate), 直接进入render;建议少用;正确使用 diff 算法 - 状态更新不使用跨层级移动节点的操作对于条件渲染多个节点时,尽量采用隐藏等方式切换节点,而不是替换节点;尽量避免将后面的子节点移动到前面的操作,当节点数量较多时,会产生一定的性能问题;其他图片的懒加载、预加载、插件的按需引入和vue相同原理

三、 webpack / vite 方向

vite 自身已经做了很大程度的优化,所以主要的还是 webpack 方向;

生产环境关闭 sourceMapSourceMap 建立错误-代码之间的映射,方便代码调试;适用于开发阶段;SourceMap 占体积大头;关闭之后你会发现项目小了很多;对图片进行压缩image-webpack-loader 插件cdn加载框架、插件资源vue|react|UI框架|可视化插件等;通过cdn的方式在script标签中直接使用,减少打包体积,提高加载速度;webpack-bundle-analyzer 构建结果分析webpack-bundle-analyzer 打包后会生产一个本地服务,清楚的展示打包文件的包含关系和大小;比较大参照组件拆分思想,进行拆分;以及对应插件或者工具类按需导入;vite 对应的是 rollup-plugin-visualizer 插件, 功能类似DllPlugin 提取公用库开发过程中,我们经常需要引入大量第三方库,这些库并不需要随时修改或调试,我们可以使用DllPlugin和DllReferencePlugin单独构建它们,配置webpack.dll.config.jscompression-webpack-plugin 开启 gzip 压缩开启gzip压缩可以有效压缩资源体积,压缩比率在3到10倍左右,可以大大节省服务器的网络带宽,提高资源获取的速度;压缩成功Response Headers 中可以看到 Content-Encoding: gzipNginx 配置如下// nginx配置开启gzip压缩,nginx会根据配置情况对指定的类型文件进行压缩gzip on; #开启或关闭gzip on offgzip_disable "msie6"; #不使用gzip IE6gzip_min_length 100k; #gzip压缩最小文件大小,超出进行压缩(自行调节)gzip_buffers 4 16k; #buffer 不用修改gzip_comp_level 8; #压缩级别:1-10,数字越大压缩的越好,时间也越长gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; #  压缩文件类型gzip_vary off;Happypack 将 loader 由单进程转为多进程webpack 的缺点是单线程的,我们可以使用 Happypack 把任务分解给多个子进程去并发执行,大大提升打包效率;配置的方法是把 loader 的配置转移到 HappyPack 中去;HardSourceWebpackPlugin 构建缓存为模块提供中间缓存步骤, 能做到第二次打包速度倍数提升;babel-loader给 loader 减轻负担babel-loader 允许使用 Babel 和 webpack 转译 JavaScript 文件;cacheDirectory 指定的目录将用来缓存 loader 的执行结果。后面的构建将会尝试读取缓存,来避免在每次执行时,可能产生的、高性能消耗的 Babel 重新编译过程, babel-loader 提速至少两倍;cache-loader 也能达到相同优化目的babel-plugin-transform-runtime / tree-shaking 减少冗余代码webpack2 默认已经支持 tree-shakingspeed-measure-webpack-plugin 构建速度分析很清楚的知道哪个模板构建花了多少秒, 针对性的优化

四、 Http 方向

DNS 预解析通过 Html meta 标签来告知浏览器, 当前页面要做DNS预解析;<meta http-equiv="x-dns-prefetch-control" content="on" />页面header中使用link标签来强制对DNS预解析<link rel="dns-prefetch" href="" />使用HTTP2解析速度快服务器解析 HTTP1.1 的请求时,必须不断地读入字节,直到遇到分隔符 CRLF 为止。而解析 HTTP2 的请求就不用这么麻烦,因为 HTTP2 是基于帧的协议,每个帧都有表示帧长度的字段多路复用HTTP1.1 的 Pipelining 技术会有阻塞的问题,处理请求响应是按照顺序的,也就是后发的请求有可能被先发的阻塞住;HTTP/2的多路复用可以粗略的理解为非阻塞版的Pipelining。即可以同时通过一个HTTP连接发送多个请求,谁先响应就先处理谁,这样就充分的压榨了TCP这个全双工管道的性能;首部压缩HTTP2 提供了首部压缩功能;HTTP/1.x每次请求,都会携带大量冗余头信息,浪费了很多带宽资源;减少HTTP请求数量HTTP会经历4 大步骤客户端连接到Web服务器发送HTTP请求服务器接受请求并返回HTTP响应释放连接TCP链接;HTTP请求建立和释放需要时间, 并且随着网络情况而变化,网路差花费的时间将更长;不阻塞情况下异步预先请求 或者合并请求;使用 http缓存充分利用好 http缓存 能有效减轻服务器、浏览器压力(需要后台配合);原理:http缓存都是在第二次请求开始的,第一次服务器会在资源返回的响应中携带上四个常用的响应头,浏览器会通过判别这些响应值来决定资源缓存的状态,再次请求的时候浏览器会带上这些响应头;**Memory Cache**(内存缓存) 是浏览器最先尝试命中的缓存,也是响应最快的缓存。但是存活时间最短的,当进程结束后,tab 标签关闭后,缓存就不存在了,因为内存空间比较小,通常较小的资源放在内存缓存中,比如 base64 图片等资源**Service Worker**(离线缓存) Service Worker 是一种独立于主线程之外的 Javascript 线程。它脱离于浏览器窗体,因此无法直接访问 DOM。**Disk Cache**(磁盘缓存) 内存的优先性,导致大文件不能缓存到内存中,那么磁盘缓存则不同。虽然存储效率比内存缓存慢,但是存储容量和存储市场有优势。Push Cache(推送缓存)它是最后一道缓存向浏览器发送一个数据上次被修改的时间;浏览器就知道了该数据最后被修改的时间,后续请求中,会和服务器进行时间的比较,如果服务器上的时间比本地时间要新,说明数据有更改,浏览器需要重新下载数据;缺点:当服务器响应中有 Expires 或者 Cache-Control 设置了 max-age 响应头的时候,浏览器不会向服务器发起校验请求,而是直接复用本地缓存。如果此时服务器进行了资源的更新,用户就无法获取到最新的资源,只能通过强制刷新浏览器缓存来跟服务器请求最新的资源浏览器会在后续的请求中携带上这个参数来确定缓存是否需要更新;需要注意的是,ETag只有在本地缓存已过期(Expires)或者缓存模式设置为 no-cache(Cache-Control)的时候,才会被浏览器携带上服务器端的值进行判别;标记了数据的过期时间,超过其中规定的时间后,缓存会被定义为过期,优先级Cache-Control的max-age > Expires可以携带多个响应值,这些值可以设置缓存时间、状态以及验证状态;public: 所有内容都将被缓存包括客户端、代理cdn节点private: 只缓存到客户端,不缓存到代理服务器no-cache: 需要先与服务器确认no-store: 所有内容都不被缓存max-age: 在多少秒之后失效Cache-Control (强缓存)Expires (强缓存)ETag(协商缓存)--> 值是一个字符串(数据的哈希值),每个数据都有一个单独的标志Last-Modified (协商缓存)缓存的位置按照获取资源请求优先级,缓存位置依次如下:优先使用get请求get请求不需要预检和交互; 频繁刷新浏览器不会对浏览器、服务器造成太大的压力,无伤;使用 CDN 服务器端缓存加快访问速度 (俗称边缘计算)原理:CDN网络是在用户和服务器之间增加了一层缓存层,将用户的请求引导到最优的缓存节点就近获取所需要的内容而不是服务器源站,从而降低网络用塞、加块访问速度响应用户的请求 (俗称负载均衡);过程:先向CDN边缘节点发起请求 -> 检测是否过期 -> 没有直接返回 -> 过期则去根服务器获取数据再返回;设置:通过http响应头中的Cache-Control和max-age的字段来设置CDN边缘节点的数据缓存时间;例子:网站中大量的css,html,js等文件、大文件的下载(图片、视频、音频等),将这些静态内容推送到CDN节点;构成:初始服务器,分布于各个节点的缓存服务器,重定向DNS服务器和内容交换服务器主要技术:内容分发技术(构建网络,将链接到IP网络上的内容,快速的传输到用户终端)内容存储技术 (内容源的存储、内容在cache节点中的分布式存储);负载均衡;缺点:当源服务器资源更新后,如果 CDN 节点上缓存数据还未过期,用户访问到的依旧是过期的缓存资源,这会导致用户最终访问出现偏差。因此,开发者需要手动刷新相关资源,使 CDN 缓存保持为最新的状态减少 DNS 查找次数DNS用于映射主机名和IP地址, DNS解析有代价,一般一次解析需要20~120毫秒。浏览器在DNS查询完成前不会下载任何东西,所以浏览器会想办法对DNS的查找结果进行缓存减少域名主机可减少DNS查询的次数,最理想的方法就是将所有的内容资源都放在同一个域(Domain)下面,这样访问整个网站就只需要进行一次DNS查找,这样可以提高性能。在HTTP /1.1中放在同个域下面会带来一定数量的并行度(它的建议是2),那么就会出现下载资源时的排队现象,这样就会降低性能,推荐客户端针对每个域在一个网站里面使用至少2个域,但不多于4个域请求返回体的压缩、分页、缓存当一个请求返回数据比较多(如数据字典、图片、execl表格、pdf、音频、视频)时;优先和后台协商使用缓存、分页、切片等方案;并且异步请求放到请求队尾, 当接收数据很大时,你会发现浏览器会进入卡死状态;

四、 图片方向

优先使用雪碧图图片、图标的切换优先使用雪碧图代替、以减少体积, 提高响应速度;

使用font字体、svg、base64、JPG、JPEG、WEBP格式的图片

列表图片使用预加载、懒加载、及脱离文档流后进行DOM回收

大批量图片渲染是很耗浏览器性能的,并且会阻塞其他渲染,这时肯定需要使用预加载、懒加载方法;对图片进行操作时,尽量脱离文档流后进行 DOM 回收,避免重绘重排;使用http、cdn缓存、不失帧情况下对图片压缩缓存、压缩目的都是快速响应用户操作显示图片.

五、浏览器渲染方向

SSR(优化首页渲染时间)、骨架屏、开启gzip压缩、js混淆(无效字符及注释的删除、码语义的缩减和优化)css的文件放在头部、css压缩、合并css资源减少重定向、减少外链、不滥用web字体、,js文件放在尾部或者异步(async和defer、动态脚本创建) ( 标签 preload 渲染前加载,prefetch,dns-prefetch渲染完成后空闲时间加载 )避免內联样式、避免html里执行js, 多次修改样式、结构,尽量合并在一起修改;使用css动画、减少css表达式、使用requestAnimationFrame操作动画避免重绘重排、减少 DOM 元素个数批量操作DOM,脱离文档流后在操作;使用css3 GPU硬件加速translate3d、translateZ、rotate、scale、transform、opacity、filters等动画效果不会引起回流重绘对于频繁操作使用节流、防抖节流:短时间内大量触发同一事件,在函数执行一次之后,该函数在指定的时间期限内不再执行,直至过了这段时间才重新生效(例如监听滚动条 scroll 事件);防抖:如果短时间内大量触发同一事件,只会执行一次函数(例如:input事件);长列表优化vue|raect 对应的UI框架基本都提供了虚拟列表组件, 优先使用虚拟列表组件;使用JSON格式JSON是一种轻量级的数据交换格式,是理想的数据交换格式。同时,JSON是 JavaScript原生格式,这意味着在 JavaScript 中处理 JSON数据不需要任何特殊的 API 或工具包控制Cookie大小和污染Cookie是本地的磁盘文件,每次浏览器都会去读取相应的Cookie,所以建议去除不必要的Coockie,使Coockie体积尽量小使用Cookie跨域操作时注意在适应级别的域名上设置coockie以便使子域名不受其影响Cookie是有生命周期的,所以请注意设置合理的过期时间,合理地Expire时间和不要过早去清除coockie其他避免 404、减少 DOM 访问、用 <link> 代替 @import、保持单个内容小于25K

六、 Chrome Performance 分析

多使用 Chrome Performance 的火焰图 查找性能瓶颈,针对性的优化;

评测报告中FP、FCP、FMP、LCP、TTI、TTFB、FCI、FID、DCL、Speed IndexFP  "首次绘制" 是第一个“时间点”,它代表浏览器第一次向屏幕传输像素的时间,就是页面在屏幕上首次发生视觉变化的时间。FCP "首次内容绘制", 代表浏览器第一次向屏幕绘制 “内容” (只有首次绘制文本、图片(包含背景图)、非白色的canvas或SVG时才被算作 FCP)FP和FCP可能是相同的时间,也可能是先FP后FCP。FMP "首次有效绘制" 主要内容”开始出现在屏幕上的时间点。它是我们测量用户加载体验的主要指标LCP 可视区“内容”最大的可见元素开始出现在屏幕上的时间点。TTI "可交互时间" 网页第一次 完全达到可交互状态 的时间点TTFB 表示浏览器接收第一个字节的时间FCI 告诉我们页面什么时候完全达到可用FID FID指的是用户首次与产品进行交互时,我们产品可以在多长时间给出反馈DCL DomContentloaded事件触发的时间Speed Index 页面可见部分的平均时间商城、官网、博客这种页面更侧重FMP(用户希望尽快看到有价值的内容),而类似后台管理系统或在线PPT这种产品则更侧重TTI(用户希望尽快与产品进行交互)。白屏时间计算将代码脚本放在 </head> 前面就能获取白屏时间:<script>    new Date().getTime() - performance.timing.navigationStart</script>首屏时间计算在window.onload事件中执行以下代码,可以获取首屏时间:new Date().getTime() - performance.timing.navigationStart

部分参考:

文章