前端优化总结

153 阅读12分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。详情

关于前端优化的地方和可涉及的知识点就太多了,本文在下面几个方面来考虑前端性能的优化:

  1. 从输入url到页面展示过程更快的获取资源优化资源加载时机、加载方式等

  2. 代码文件:尽量减小资源的体积大小和请求次数优化资源加载时机、加载方式等

前端性能优化最终的目的无非是更快更稳定的展示页面给用户。

在输入url到页面展示的过程有:DNS解析、TCP连接、HTTP请求、TCP关闭、浏览器渲染这些过程。在此过程中的优化大多是如何更快的连接,更快的获取资源。

1、DNS预解析

DNS 解析也是需要时间的,如果解析域名过多,会让首屏加载变得过慢,可以考虑DNS预解析dns-prefetch优化,来预先获得域名所对应的 IP

DNS Prefetch 应该尽量的放在网页的前面,推荐放在meta后面。具体使用方法如下:

<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="//www.zhix.net">
<link rel="dns-prefetch" href="//api.share.zhix.net">
<link rel="dns-prefetch" href="//blog.poetries.top">

2、TCP连接/HTTP连接

使用HTTP2

HTTP1.1时代,如果HTTP1.1同时发起多个请求,就得建立多个TCP连接,一个TCP连接只能处理一个HTTP1.1的请求,消耗了好几个 RTT 时间,并且由于 TCP 慢启动的原因,加载体积大的文件会需要更多的时间。

使用HTTP2带来的好处:

  1. 多个请求可以共有一个TCP连接,被称为多路复用。可以节省多次TCP建立连接的耗时。
  2. 二进制分帧,将所有要传输的消息采用二进制编码,并且会将信息分割为更小的消息块。
  3. 支持 Header 压缩,进一步的减少了请求报文的数据大小
  4. 服务端推送。服务端可以在客户端发起请求前发送数据,换句话说,服务端可以对客户端的一个请求发送多个相应,并且资源可以正常缓存。

【注】使用 http2 的前提是必须是 https。

资源缓存

  • 缓存对于前端性能优化来说是个很重要的点,良好的缓存策略可以降低资源的重复加载提高网页的整体加载速度
  • 通常浏览器缓存策略分为两种:强缓存和协商缓存

强缓存

  1. 实现强缓存可以通过两种响应头实现:ExpiresCache-Control 。强缓存表示在缓存期间不需要请求,state code200
  2. ExpiresHTTP / 1.0 的产物,Expires表示具体的绝对时间(xx年xx月xx日xx时xx分xx秒)。并且 Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效。后续HTTP中cache-control字段代替express的作用,且修复了express的局限性
expires: Wed, 11 Sep 2019 16:12:18 GMT  //设置过期时间为 Wed, 11 Sep 2019 16:12:18
cache-control: max-age=31536000  // 设置max-age过期时间长度,单位秒

协商缓存

  • 如果缓存过期了,我们就可以使用协商缓存来解决问题。协商缓存需要请求,如果缓存有效会返回 304
  • 协商缓存需要客户端和服务端共同实现,和强缓存一样,也有两种实现方式:
  1. Last-ModifiedIf-Modified-Since

    • Last-Modified 表示本地文件最后修改日期,If-Modified-Since 会将 Last-Modified的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来
    • 但是如果在本地打开缓存文件,就会造成 Last-Modified 被修改,所以在 HTTP / 1.1 出现了 ETag
  2. ETagIf-None-Match

    • ETag 类似于文件指纹,If-None-Match 会将当前 ETag 发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。并且 ETag 优先级比 Last-Modified

对于大部分的场景都可以使用强缓存配合协商缓存解决,但是在一些特殊的地方可能需要选择特殊的缓存策略。我们要在实际项目优化中根据具体情况来选择缓存方式。

更多缓存详情点击查看:浏览器缓存机制

减少HTTP请求次数

  1. 整合图片,如css精灵的方式将很多个图片合并成一个,减少请求次数
  2. 合并脚本和样式表:javascript和css可以嵌入html文档中内联 ,还可以放入外部脚步样式表中,前者会增加文档大小,并且不符合低耦合的开发思路,使得代码较难维护。需要根据实际项目情况做选择
  3. 配置多个域名和CDN加速。建议利用CDN来加载不是经常更新修改的静态资源(如图片、css、第三方js库等)
  4. 代码中接口请求可以使用防抖和节流

3、浏览器渲染

服务端渲染SSR

使用服务端渲染:服务端返回 HTML 文件,客户端只需解析 HTML。

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

Vue的Nuxt.js和React的next.js都可以实现服务器渲染方法

资源预加载

  • 在开发中,可能会遇到这样的情况。有些资源不需要马上用到,但是希望尽早获取,这时候就可以使用预加载
  • 预加载其实是声明式的 fetch ,强制浏览器请求资源,并且不会阻塞 onload 事件,可以使用以下代码开启预加载

1、preload:页面加载的过程中,在浏览器开始主体渲染之前加载。

<link rel="preload" href="http://example.com">

2、prefetch:页面加载完成后,利用空闲时间提前加载。

预加载可以一定程度上降低首屏的加载时间,因为可以将一些不影响首屏但重要的文件延后加载,唯一缺点就是兼容性不好

异步加载js的方式有deferasync,这两个属性使得script不会阻塞DOM渲染。

3、defer

  • defer只适用于外联脚本,如果script标签没有指定src属性,只是内联脚本,不要使用defer(内联脚本不会按照声明顺序执行)
  • 如果有多个声明了defer的脚本,则会按顺序下载和执行
  • defer脚本会在DOMContentLoadedload事件之前执行

假设有三个js文件

// a.js
console.log('a')
​
// b.js
setTimeout(()=>console.log('b'),100)
​
// c.js
console.log('c')

使用defer属性异步加载:

<html>
    <head>
        <script src="js/a.js" defer></script>
        <script src="js/b.js" defer></script>
        <script src="js/c.js" defer></script>
        <script>
            console.log('d')
        </script>
    </head>
    <body>
        <script>
            doucment.addEventListener("DOMContentLoaded", function(){
                console.log('DOMContentLoaded')
            }, false)
            window.addEventListener("load",function() {
                console.log('load')
            })
        </script>
    </body>
</html>

输出结果:d a b c DOMContentLoaded load

4、async

  • 只适用于外联脚本,这一点和defer一致

  • 如果有多个声明了async的脚本,其下载和执行也是异步的,不能确保彼此的先后顺序

  • async会在load事件之前执行,但并不能确保与DOMContentLoaded的执行先后顺序

将上面代码中的defer属性换成async

<script src="js/a.js" async></script>
<script src="js/b.js" async></script>
<script src="js/c.js" async></script>

输出结果:d DOMContentLoaded a b c load。abc之间的输出顺序不确保的

总结

  • deferasync在网络读取的过程中都是异步解析
  • deferasync差别在于脚本下载完之后何时执行。defer是有顺序依赖的,async只要脚本加载完后就会执行
  • preload 可以对当前页面所需的脚本、样式等资源进行预加载
  • prefetch 加载的资源一般不是用于当前页面的,是未来很可能用到的这样一些资源

预渲染

可以通过预渲染将下载的文件预先在后台渲染,可以使用以下代码开启预渲染

<link rel="prerender" href="http://poetries.com">

预渲染虽然可以提高页面的加载速度,但是要确保该页面百分百会被用户在之后打开,否则就白白浪费资源去渲染

懒执行和懒加载

懒执行

  • 懒执行就是将某些逻辑延迟到使用时再计算。该技术可以用于首屏优化,对于某些耗时逻辑并不需要在首屏就使用的,就可以使用懒执行。懒执行需要唤醒,一般可以通过定时器或者事件的调用来唤醒

懒加载

  • 懒加载就是将不关键的资源延后加载

懒加载的原理就是只加载自定义区域(通常是可视区域,但也可以是即将进入可视区域)内需要加载的东西。对于图片来说,先设置图片标签的 src 属性为一张占位图,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为 src 属性,这样图片就会去下载资源,实现了图片懒加载

  • 懒加载不仅可以用于图片,也可以使用在别的资源上。比如进入可视区域才开始播放视频等

4、文件优化

除了在以上浏览器解析/网络传输中的优化,我们还可以在代码中注意一些编码习惯和做一些处理来达到前端性能的优化。

图片优化

对于图片优化,可以考虑两个点:①减少像素点 ②减少每个像素点能够展示的颜色

  1. 使用css代替一些装饰类的可代替的图片

  2. 小图使用base64格式,图标小图可换为字体图标

  3. 多个图标文件可以整合到一张图片中(css精灵/雪碧图)

  4. 对于移动端,图片可以使用CDN加载。屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。

  5. 选择正确的图片格式

    • 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好
    • 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替
    • 照片使用 JPEG

代码文件

  • 操作DOM时注意减少重绘和重排
  • 使用事件委托节省内存:事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
  • 使用防抖或节流减少http请求或者用户交互操作,提升体验感
  • 去掉console.log的输出
  • 动画效果注意浏览器的刷新频率60fps,可以有效利用requestAnimationFrame、requestIdleCallback来做处理
  • 其他用户感知上的优化:如loading效果、骨架框加载效果等

其他文件优化

  • CSS文件放在 head
  • 服务端开启文件压缩功能
  • script 标签放在 body 底部,因为 JS 文件执行会阻塞渲染。当然也可以把 script 标签放在任意位置然后加上 defer ,表示该文件会并行下载,但是会放到 HTML 解析完成后顺序执行。对于没有任何依赖的 JS文件可以加上 async ,表示加载和渲染后续文档元素的过程将和 JS 文件的加载与执行并行无序进行。 执行 JS代码过长会卡住渲染,对于需要很多时间计算的代码。
  • 可以考虑使用 WebworkerWebworker可以让我们另开一个线程执行脚本而不影响渲染。

CDN

静态资源尽量使用 CDN 加载,由于浏览器对于单个域名有并发请求上限,可以考虑使用多个 CDN 域名。对于 CDN 加载静态资源需要注意 CDN 域名要与主站不同,否则每次请求都会带上主站的 Cookie

5、其他优化

webpack优化

在现代框架项目的基础上基本上是用webpack实现打包的,那么我们可以在打包过程中进行优化,达到更小的包体积,更快的加载资源。

1、tree shaking

使用tree shaking用于清除我们项目中的一些无用代码,它依赖于ES中的模块语法。

2、打包模式

线上使用 production 模式打包项目,这样会自动开启代码压缩

3、优化图片

  • 优化图片,对于小图可以使用 base64 的方式写入文件中
  • 压缩图片后上传至CDN
  • 对于不能压缩的大图片,可以使用图片分割提高图片加载效率

4、拆包

  • 按照路由拆分代码,实现按需加载
  • 给打包出来的文件名添加哈希,实现浏览器缓存文件

前端监控

前端监控错误捕获,可以在一定程度上避免代码运行错误造成页面异常或白屏

  • 对于代码运行错误,通常的办法是使用 window.onerror 拦截报错。该方法能拦截到大部分的详细报错信息。
  • 对于跨域的代码运行错误会显示 Script error. 对于这种情况我们需要给 script 标签添加 crossorigin 属性
  • 对于某些浏览器可能不会显示调用栈信息,这种情况可以通过 arguments.callee.caller 来做栈递归
  • 对于异步代码来说,可以使用 catch 的方式捕获错误。比如 Promise 可以直接使用 catch 函数,async await 可以使用 try catch
  • 但是要注意线上运行的代码都是压缩过的,需要在打包时生成 sourceMap 文件便于 debug
  • 对于捕获的错误需要上传给服务器,通常可以通过 img 标签的 src发起一个请求

除了前端监控错误外,我们还可以通过前端性能、用户行为的监控来指导我们项目优化的方向。

写在最后

我们在实际上的项目优化过程中不能盲目追求完美,要对症下药,选择适合当前项目的优化方式,考虑性价比,才能达到最优解