关于前端优化

416 阅读12分钟

前端优化范围往大了分可分为两种:网络优化和渲染优化

网络优化:从Url输入到页面渲染

   主要有三个过程:DNS 解析 -> TCP 连接 -> HTTP 请求/响应

   对于DNS解析和TCP连接前端可以优化的地方有限,所以主要专注HTTP优化。

   而HTTP优化可分为两个大致方向:

  1. 减少请求次数
  2. 减少单次请求时间

  以上两个方向直接指向了日常开发中非常常见的操作,资源的压缩和合并----webpack

  babel-loader :  exclude:/(node_modules|bower_components)/ 跳过文件夹,以及添加参数 loader: 'babel-loader?cacheDirectory=true' 缓存转译结果至文件系统。

  dllPlugin&&dllReferencePlugin结合处理第三方依赖,因为第三方依赖不会变动,只需打包一次,避免在后续开发中重复构建,影响开发效率

//dllModel.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
   entry:{
       //依赖的库数组
        vendor:['react']
    },
     output: {
      path: path.join(__dirname, 'dist'),
      filename: '[name].js',
      library: '[name]_[hash]',
    },
    plugins:[
        new webpack.dllPlugin({
        name:'[name]_[hash]',
        path:path.join(__dirname,'dist','[name]-manifest,json'),
        context:__dirname
    })
    ]
}

 运行webpack --config dllModel.config.js 后会在dist文件夹下面生成vendor-manifest.json和vendor.js,分别是预编译包配置和文件,配合主包使用如下配置:

//webpack.prod.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
  mode: 'production',
  // 编译入口
  entry: {
    main: './src/index.js'
  },
  // 目标文件
  output: {
    path: path.join(__dirname, 'dist/'),
    filename: '[name].js'
  },
  // dll相关配置
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      // manifest就是我们第一步中打包出来的json文件
      manifest: require('./dist/vendor-manifest.json'),
    })
  ]
}

此后打包时都会绕过vendor-manifest.json内预编译好的第三方包。

webpack是单线程的,就算存在多个任务也只能一个接一个等待处理,但我们机器cpu是多核的,因此可以将任务分解给多个子进程并发执行,大大提高打包效率,happypack-将loader由单线程转为多线程

const HappyPack = require('happypack');
const os = require('os');
const HappyThreadPool = HappyPack.ThreadPool({size:os.cpus().length});
module.exports = {
    module:{
        rules:[
            ...,
            { 
                test:/\.js$/,
                loader:'happypack/loader?id=happyBabel',
            }        
        ]    
    },
    plugins:[
        ...,
        new HappyPack({
            id:'happyBabel',
            //指定进程池
            threadPool:HappyThreadPool,
            loaders:['babel-loader?cacheDirectory']
        })
    ]
}

webpack4 对于mode:'production'启用了treesharking去除了未被引用的模块,默认支持uglifyjs-webpack-plugin处理粒度更细的冗余代码

const UglifyPlugin = require('uglify-webpack-plugin')
module.exports = {
       plugins:[
        ...,//其他配置
        new uglifyPlugin({
            //允许并发
            parallel:true,
            //开启缓存
            cache:true,
            compress:{
                //去除所有console
                drop_console:true,
                //把多次定义的静态值设置为变量
                reduce_vars:true
            },
            output:{
                //不保留注释
                comment:false,
                //使输出的代码尽可能紧凑
                beautify:false
            }
        })]
}

按需加载:页面之间不存在相互依赖的关系,不需要一次性加载完成,只加载页面需要的文件,本质上是减少了页面渲染需要加载的文件,减少了http请求次数和单次请求的时间

require.ensure(dependencies, callback, chunkName)引入组件

output: {
    path: path.join(__dirname, '/../dist'),
    filename: 'app.js',
    publicPath: defaultSettings.publicPath,
    // 指定 chunkFilename
    chunkFilename: '[name].[chunkhash:5].chunk.js',
},

对于有一定规模大小的文件启用gzip压缩,可以有效压缩70%左右,只需要在请求的时候在头部加上 accept-encoding:gzip,这样会增加服务器的消耗,而webpack本身启用gzip压缩js,css等文件,原理上就是对代码重新编码,用更少的字节替换。

图片的权衡

      图片是前端页面最重要的部分,前端优化首先不是提高js,css下载速度或者减少下载时间,而是尽可能快的让图片和文字展示给用户。

     时下web的图片格式大致有jpg/jpeg,png,svg,webp,base64,像素是由二进制来表示,不同格式的图片所能展示的颜色数量不同,如果一个图片格式支持n位二进制,那么它可以支持2^n种颜色

     jpg/jpeg:体积小,加载快,有损压缩,24位,不支持透明

     应用场景:使用jpg/jpeg格式的图片来呈现大图,既可以保住图片的质量又可以避免体积过大,是时下banner图,背景图的选项

     不适合场景:logo和矢量图形等颜色对比强烈,线条感强的场景

    png:无损压缩,质量高,体积大,支持透明,分为 8 位和24位

     应用场景:适用于颜色简单,对比度强,通常应用logo

     svg:矢量图,无限放缩不失真,文本文件,体积小,兼容性好

     基于xml语法的文件格式,对图像的处理不基于像素,而是对图形的描述,svg和jpg,png相比,体积更小,可压缩性更强,缺点是渲染成本比较高,并且可编程,需要一定学习成本。

    base64: 编码格式,文本文件,小图标解决方案

    像大量图标可以合并为一个雪碧图,从而较少http请求次数,而base64可以直接写入css或者html,减少了http请求次数。

     缺点:使用base64编码图片后,文件大小会膨胀到原来的4/3,所以通常处理非常小,更新频率低的图片

     webp:google在2010年专为web开发的图片格式,旨在**加快图片加载速度,**相比其他图片格式,webp支持有损和无损压缩,支持图片透明,压缩的体积更小,唯一缺陷是兼容性差,google 49及以上支持,大部分浏览器都不支持。

    处于性能优化以及兼容,在用户请求图片资源时会头部携带Accept字段,如果包含image/webp,则返回webp格式的文件。

浏览器缓存

       memory cache 

       Service Woker cache 

       HTTP cache 

       Push cache 

http cache 分为强缓存和协商缓存,优先级高的时强缓存,命中强缓存失败的情况下才会触发协商缓存

强缓存是由http 头中的expires和cache-control 来控制的,初次请求会根据头部内容选择保留,再次请求时会根据之前保留的头部判断目标资源是否命中强缓存,若命中则直接从缓存中获取,不会再与服务端发生通信

expires 会相对于客户端本地时间来判断资源是否过期,再次请求资源时会比对本地时间和expires内的时间戳,但客户端和服务端有可能时间不一致,并且存在客户端修改时间导致资源过期。

cache-control:max-age=n 可以设置在多少秒内资源有效,s-maxage 优先级高于max-age,区别在于s-maxage设置的资源被代理服务器缓存的,针对public类型的资源。资源默认是private的,即只能被浏览器缓存,带有public特性则能被代理服务器缓存。

no-cache 只会走协商缓存,no-store 则每次请求都是下载最新资源。

协商缓存是浏览器和服务端进行通信,判断资源是否重新获取,一般是资源过期引发协商缓存,如果服务端提示资源没有发生改动,资源会被重定向浏览器缓存,并且状态码是304,

304 not modified 重定向。

判断资源是否变更最开始是判断Last-Modified,来判断,即资源的最后一次编辑时间,但存在编辑了文件,但内容没有改变,会导致重新请求,并且无法区分一秒内的多次修改,所以etag作为补充,相当于资源的唯一标识符,etag优先级高于last-modified,但会导致额外的服务端开销。

 memory cache 指在内存中的缓存,响应速度最快,生命最最短,和渲染进程同时存在,当tab页关闭即释放,浏览器最先尝试命中的区域,一般base64格式的图片都能被塞进内存,也包括体积不大的js,css,本着节约原则

Service Worker cache  创建独立于主线程之外的js线程,本质上是浏览器和服务器之间的代理服务器,webworker的一种,它脱离于窗体,因此不能直接操作Dom,是的serviceworker 的行为无法干扰页面的性能,可以帮助我们实现离线缓存,消息推送和网络代理等功能。

serviceworkercache 的生命周期有 : install,active,working,servicework一旦被install,就会一直存在,然后在active和working之间切换。

window.navigator.serviceWorker.register('/test.js')注册子线程

//test.js
self.addEventListener('install',(event)=>{
    event.waitUntil(
// 考虑到缓存也要更新,通过版本号控制
        caches.open('test-v1').then(cache=>{
            //需要缓存的文件列表
          return caches.addAll(['./test.js','./test.css'])
        })
    )

})
//serviceworker可以监听所有的接口请求,监听fetch
self.addEventListener('fetch',event=>{
    event.respondWith(
        //查看是否命中缓存
        caches.match(event.request).then(res=>{
            //    如果命中了,会有返回
            if(res)return res
            //没命中就继续向服务端发起请求
            return fetch(event.request).then(response=>{
                if(!response||response.status !== 200) return response
                //请求成功的话将结果缓存起来
                caches.open('test-v1').then(cache=>{
                    cache.put(event.request,response)
                })
                return response.clone();
            })
        })
    )
})

Push Cache 其他三种缓存都未命中才会考虑push cache,基于http2,不同页面共享同一个http2连接就可以共享同一个Push Cache.

除了缓存,本地存储也是优化的措施

Cookie,localStorage,SessionStorage,indexedDb

Cookie 的本职工作是为了维持状态,但只能存储4kb左右的内容,键值对形式,同一个域名下的所有请求都会附带cookie,对于一些不需要cookie的静态资源也会加上,这造成了额外的开销。

localStorage和sessionStorage在api上面是没有差异的,区别在于localStorage是持久的,sessionStorage只能是同一个窗口下共享,大约能存储5~10M的内容。可以用来存储一些base64的图片。

IndexedDB 运行在浏览器上的非关系数据库,一般没有大小限制,大于250M,如果设计本地存储无法解决的程度,则可以使用indexedDB。

CDN 的实际应用

静态资源本身具有访问频率高、承接流量大的特点,因此静态资源加载速度始终是前端性能的一个非常关键的指标。CDN 是静态资源提速的重要手段,在许多一线的互联网公司,“静态资源走 CDN”并不是一个建议,而是一个规定。

原理是分布在全国各地的服务器,选择离用户最近的一个,同时会对过期的资源进行回源,即重新获取,保证资源的即时性。

选择和业务域名不同的cdn服务器,这样可以避免携带cookie。

服务端渲染

对于一些文章或者内容来获取流量的网站推荐使用ssr(服务端渲染),优化首屏加载速度,因为本来需要客户端处理的事交由服务端处理了,同时对搜索引擎友好,搜索引擎不会执行页面的js,因此SSR天然可以更好的被爬虫录取。原理就是利用框架的render模块将vnode转化为真实dom,现在node上跑一遍。

服务端渲染缺点就是吃服务器资源,因为需要让服务器去运行render,单核cpu只能支持几十甚至十几的QPS(每秒请求数)。

CSS优化

因为css引擎查找样式表的规则是从右往左,因此,更少的层级能带来更快的处理速度,多用id选择器,少用标签选择器,利用class提取可复用的css。

css是阻塞的资源,即便dom解析完毕了,也需要等待css解析完毕,主要是避免没有样式页面过于丑陋,因此要尽可能早的解析完css,所以有将css资源引用放到head内,或者CDN加速。

JS优化

在不做声明的情况下,js也是阻塞的,当HTML解析器遇到script标签时会暂停渲染,把控制权交给js引擎,js引擎对内联js会立即执行,对外部脚本需要先获取后执行,等到js执行完毕,浏览器才会将控制权交还渲染引擎。

但是我们会知道这个脚本何时执行,因此可以让它异步,从而不阻塞渲染进程。

async 模式下,JS 不会阻塞浏览器做任何其它的事情。它的加载是异步的,当它加载结束,JS 脚本会立即执行

<script async src="./test.js"></script>

defer 模式下,JS不会阻塞浏览器做其它事情,加载异步,当它加载结束时,需要等到整个文档解析完毕,DOMcontentLoaded事件即将被触发时,才会执行有defer标记的js文件。

<script defer src="./test.js"></script>

回流和重绘

每次进行Dom操作的时候都会通过交接接口去修改渲染引擎内的渲染树,对于批量Dom操作,可以将每次改动的数据缓存下来,批量处理完成后再去操作真实Dom,或者利用Fragment创建独立于真实DOM树的最小文档对象,来缓存批量化的DOM操作,document.createDocumentFragment创建一个新的容器,允许我们在这个容器内进行任意的Dom操作,而不用担心重绘和回流。

对于“离线”的DOM进行操作不会触发回流和重绘,原理上就是dispaly:none,将dom离线,操作完毕后再display:block,将dom显示。

事件循环

事件循环中异步队列有两种:macro(宏任务),micro(微任务)

       常见的macro:setTimeout,setInterval,setImmediate,script(整体代码),I/O操作,UI 渲染等。

        常见的micro:process.nextTick,Promise,mutationObsever等

一个完整的Event Loop 可以概括为下列过程:

初始状态:调用栈为空,micro队列空,macro内有一个script脚本(整体代码)

全局上下文,script被推入调用栈,同步代码先执行,执行过程中会产生新得macro和micro推入对应的队列,执行完后,script被移出队列,本质上就是队列的宏任务执行的出列的过程。

然后处理micro-task,执行macro是一个一个执行,而micro是一队一队执行,只有当前micro队列清空,才会执行下一步。

执行渲染操作,更新界面。(每当微任务执行完毕都会执行更新界面的操作,因此修改Dom的操作都应放在micro队列内,减少渲染次数)

检查是否存在web woker ,存在就执行。

懒加载,节流和防抖

节流防抖监听scroll事件,当页面高度达到指定值时请求新得数据。

在规定间隔内多次触发,会重新计时,超过间隔时间立即执行。

function throttle (fn,delay = 1000){
    //laster 为上次触发的事件,timer为计时器
    let laster = 0, timer = null
    return function (){
        let context = this,args = arguments,now = +new Date();//等同于new Date().getTime()
        //判断上次触发间隔是否小于delay
        if(now - last < delay){
            clearTimeOut(timer);
            timer = setTimeOut(function(){
                last = now;
                fn.apply(context,args)
            },delay)
        }else{
            last = now;
            fn.apply(context,args)
          }
    }
}