性能优化

1. CDN的概念

CDN(Content Delivery Network, 内容分发网络) 是指一种通过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序以及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。

  1. 典型的CDN系统由下面三个部分组成:
  • 分发服务系统:最基本的工作单元就是Cache设置,cache(边缘cache) 负责直接响应最终用户的访问请求,把缓存在本地的内容快速地提供给用户。同时cache还负责与源站点进行内容同步,把更新的内容以及本地没有的内容从源站点获取并保存在本地。Cache设备的数据、规模、总服务能力是衡量一个CDN系统服务能力的最基本的指标。
  • 负载均衡系统:主要功能是负责对所有发起服务请求的用户进行访问调度,确定提供给用户的最终实际访问地址。两级调度体系分为全部负载均衡和本地负载均衡。全局负载均衡主要根据用户就进性原则,通过对每个服务节点进行“最优”判断,确定向用户提供服务的cache的物理位置。本地负载均衡主要负责节点内部的设备负载均衡
  • 运营管理系统:运营管理系统分为运营管理和网络管理子系统,负责处理业务层面的与外界系统交互所必须的收集、整理、交付工作,包含客户管理、产品管理、计费管理、统计分析等功能。
  1. CDN的作用

    (1) 在性能方法,引入CDN的作用在于

    • 用户收到的内容来自最近的数据中心,延迟更低,内容加载更快
    • 部分资源请求分配给了CDN,减少了服务器的负载

    (2) 在安全方面,CDN有助于防御DDos、MITM等网络攻击

    • 针对DDoS: 通过监控分析异常流量,限制其请求频率
    • 针对MITM:从源服务器到CDN节点到ISP,全链路HTTPS通信

    除此之外,CDN作为一种基础的云服务,同样具有资源托管、按需扩展(能够应对流量高峰) 等方面的优势。

二、懒加载

懒加载与预加载的区别

  • 懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载
  • 预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存中获取资源

懒加载的代码实现

<div class="container">
		<img src="loading.gif" data-src="pic.png">
  	<img src="loading.gif" data-src="pic.png">
  	<img src="loading.gif" data-src="pic.png">
 		<img src="loading.gif" data-src="pic.png">
  	<img src="loading.gif" data-src="pic.png">
  	<img src="loading.gif" data-src="pic.png">
</div>
<script>
var imgs = document.querySelectorAll('img')
//用来判断bound.top<=clientHeight的函数,返回一个bool值
function isIn(el) {
        var bound = el.getBoundingClientRect();
        var clientHeight = window.innerHeight;
        return bound.top <= clientHeight;
 }
function loadImg(el) {
        if(!el.src){
            var source = el.dataset.src;
            el.src = source;
        }
 }
function check() {
  Array.from(imgs).forEach(function(el){
            if(isIn(el)){
                loadImg(el);
            }
        })
}
window.onload = window.onscroll = function () { //onscroll()在滚动条滚动的时候触发
			check()
}
</script>

三、回流与重绘

  1. 回流与重绘的概念及触发条件

    (1) 回流

    当渲染树种部分或者全部元素的尺寸、构造或者属性发生变化时,浏览器会重新渲染部分或者全部

文档的过程就称为回流。下面这些操作会导致回流:

  • 页面的首次渲染
  • 浏览器的窗口大小发生变化
  • 元素的内容发生辩护
  • 元素的尺寸或者位置发生辩护
  • 元素的字体大小发生辩护
  • 激活css伪类
  • 查询某些属性或者调用某些方法
  • 添加或者删除可见的DOM元素

在触发回流的时候,由于浏览器渲染页面是基于流式布局,所以当触发回流时,会导致周围的DOM元素重新排列,它的影响范围有两种:

  • 全局范围:从根节点开始,对整个渲染树进行重新布局

  • 局部范围:对渲染树的某部分或者一个渲染对象进行重新布局

    (2) 重绘

    当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程就是重绘。下面这些操作会导致回流:

  • color、background相关属性:background-image等

  • outline相关属性:outline-color outline-width text-decoration

  • border-radius、visibility、 box-shadow

    注意:当触发回流时,一定会触发重绘,但是重绘不一定会引发回流

  1. 如何避免回流与重绘?
  • 操作DOM时,尽量在低层级的DOM节点进行操作

  • 不要使用table布局,一个小的改动可能会使整个table进行重新布局

  • 使用CSS的表达式

  • 不要频繁操作DOM,可以创建一个文档片段documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档这种

  • 将元素先设置display:none 操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM的操作不会引发回流和重绘

  • 将DOM的多个读操作(或者写操作)放在一起,而不是读写操作穿件插着写。这得益于浏览器的渲染队列机制。

    浏览器针对页面的回流与重绘,进行了自身的优化--渲染队列

    浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。

  1. 如何优化动画

    对于如何优化动画,我们知道,一般情况下。动画需要频繁的操作DOM,就会导致页面的性能问题,我们可以将动画的position属性设置为absolute或者fixed,将动画脱离文档流,这样他的回流就不会影响到页面了

  2. documentFragment是什么?用它跟直接操作DOM的区别是什么?

    当我们把一个DocumentFRagment节点插入文档树时,插入的不是DocumentFragment自身,而是它的所有子孙节点。在频繁的DOM操作时,我们就可以将DOM元素插入DocumentFragment,之后一次性的将所有的子孙节点插入文档中,和直接操作DOM相比,将DocumentFragment节点插入DOM树时,不会触发页面的重绘,这样就大大提高了页面的性能。

四、节流与防抖

  1. 对节流与防抖的理解

    • 函数防抖是指在事件被触发n秒后再执行回调,如果在这n秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。
    • 函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节点可以使用在scroll函数的事件监听上, 通过事件节点来降低事件调用的频率。
  2. 防抖节流函数的应用场景

    防抖

    • 输入框搜索场景:用户搜索后需要请求后端接口,只执行最后输入的值
    • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
    • 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次

    节流

    • 拖拽场景:固定事件内只执行一次,防止超高频次触发位置变动
    • 缩放场景:监控浏览器resize
    • 动画场景:避免短时间内多次触发动画引起性能问题
  3. 实现节流函数和防抖函数

    函数防抖的实现

    function debounce(fn,wait) {
        let timer = null
    
        return function(){
            let context = this
            let args = [...arguments]
    
            // 如果此时存在定时器的话,则取消之前的定时器重新记时
            if(timer) {
                clearTimeout(timer)
                timer = null
            }
    
            // 设置定时器 使事件间隔指定时间后执行
            timer = setTimeout(() => {
                fn.apply(context,args)
            }, wait)
    
        }
    }

    函数节流的实现

    function throttle(fn, wait) {
        let timer = null
        return function() {
            let context = this
            let args = [...arguments]
    
            if(!timer) {
                timer = setTimeout(() => {
                    fn.apply(context, args)
                    timer = null
                }, wait)
            }
        }
    }
    
    // 时间戳版
    function throttle(fn, delay) {
        let preTime = Date.now()
    
        return function() {
            let context = this
            let args = [...arguments]
            let nowTime = Date.now()
    
            // 如果两次时间间隔超过了指定时间,则执行函数
            if(nowTime - preTime >= delay) {
                preTime = Date.now()
                fn.apply(context,args)
            }
        }
    }
  4. 防抖+节流

    防抖有时候因为触发太过频繁, 导致一次响应都没有。所以希望固定的事件必定给用户一个响应,于是就有了防抖+节点的操作

    // 防抖+节流
    function throttle(fn, delay) {
        let last = 0
        let timer = null
        return function(...args) {
            let context = this
            let now = new Date()
            if(now - last < delay) {
                if(timer) {
                    clearTimeout(timer)
                    timer = null
                }
                timer = setTimeout(() => {
                    fn.apply(context, args)
                }, delay)
            } else {
                // 这个时候表示时间到了 必须给响应
                last = now
                fn.apply(context, args)
            }
        }
    }

五、图片优化

  1. 如何对项目中的图片进行优化

    • 不用图片。很多时候会使用很多修饰类图片,其实这类修饰图片完全可以用CSS去代替

    • 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用CDN加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。

    • 小图使用base64格式

    • 将多个图标文件整合到一张图片中(雪碧图)

    • 选择正确的图片格式:

      对于能够显示WebP格式的浏览器尽量使用WebP格式。因为WebP格式具有更好的图像压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好

      小图使用PNG其实对于大部分图标这类图片,完全可以使用SVG代替

      照片使用JPEG

六、Webpack优化

  1. 如何提高webpack的打包速度

    • 优化Loader

      对于Loader来说,影响打包效率首当其冲必属Babel。因为Babel会将代码转为字符串生成AST,然后对AST继续进行转变最后再生成新的代码,代码越大,转换代码越多,效率就越低。

      首先我们优化Loader的文件搜索范围

      module.exports = {
        module: {
          rules: [
            {
              // js 文件才使用 babel
              test: /\.js$/,
              loader: 'babel-loader',
              // 只在 src 文件夹下查找
              include: [resolve('src')],
              // 不会去查找的路径
              exclude: /node_modules/
            }
          ]
        }
      }

      对于Babel来说,希望只作用在JS代码上的,然后node_modules中使用的代码都是编译过的,所以完全没有必要再去处理一遍。

      当然这样做还不够,还可以将Babel编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间

      loader: 'babel-loader?cacheDirectory=true'
    • HappyPack

      受限于Node是单线程运行的,所以Webpack在打包的过程中也是单线程的,特别是在执行Loader的时候,长时间编译的任务很多,这样就会导致等待的情况。

      HappyPack 可以将Lader的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了

      module: {
        loaders: [
          {
            test: /\.js$/,
            include: [resolve('src')],
            exclude: /node_modules/,
            // id 后面的内容对应下面
            loader: 'happypack/loader?id=happybabel'
          }
        ]
      },
      plugins: [
        new HappyPack({
          id: 'happybabel',
          loaders: ['babel-loader?cacheDirectory'],
          // 开启 4 个线程
          threads: 4
        })
      ]
    • DIIPlugin

      DIIPligin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。DIIPlugin的使用方法如下:

      // 单独配置在一个文件中
      // webpack.dll.conf.js
      const path = require('path')
      const webpack = require('webpack')
      module.exports = {
        entry: {
          // 想统一打包的类库
          vendor: ['react']
        },
        output: {
          path: path.join(__dirname, 'dist'),
          filename: '[name].dll.js',
          library: '[name]-[hash]'
        },
        plugins: [
          new webpack.DllPlugin({
            // name 必须和 output.library 一致
            name: '[name]-[hash]',
            // 该属性需要与 DllReferencePlugin 中一致
            context: __dirname,
            path: path.join(__dirname, 'dist', '[name]-manifest.json')
          })
        ]
      }

      然后需要执行这个配置文件生成依赖文件,接下来需要使用DllReferencePlugin将依赖文件引入项目中

      // webpack.conf.js
      module.exports = {
        // ...省略其他配置
        plugins: [
          new webpack.DllReferencePlugin({
            context: __dirname,
            // manifest 就是之前打包出来的 json 文件
            manifest: require('./dist/vendor-manifest.json'),
          })
        ]
      }
    • 代码压缩

      在webpack3中,一般使用UglifyJS来压缩代码,但是这个是单线运行的,为了加快效率,可以使用webpack-parllel-uglify-plugin来并行运行UglifyJS,从而提高效率。

      在webpack4中,不需要以上这些操作了,只需要将mode设置为production就可以默认开始以上功能。代码压缩也是我们必做的性能优化方案,当然我们不止可以压缩JS代码,还可以压缩HTML、CSS代码,并且在压缩JS代码的过程中,我们还可以通过配置实现比如删除console.log这类代码的功能。

    • 其他

      可以通过一些小的优化点来加快打包速度

      resolve.extensions: 用来表面文件后缀列表,默认查找顺序是['.js','.json'], 如果你的导入文件没有添加后缀就会按照这顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高德后缀排在前面

      resolve.alias: 可以通过别名的方式来映射一个路径,能让Webpack更快找到路径

      module.noParse: 如果你确定一个文件下没有其他依赖,就可以使用该属性让Webpack不扫描该文件,这种方式对于大型的类库很有帮助

  2. 如何减少Webpack打包体积

    • 按需加载

    在开发SPA项目的时候,项目中都会存在很多路由界面。如果将这些页面全部打包进一个JS文件的话,虽然将 多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了页面能更快地呈现给用户,希望首页能加载的文件体积越小越好,这时候就可以按需加载,将每个路由页面单独打包为一个文件。当然不仅仅路由可以按需加载,对于loadash这种大型类型同样可以使用这个功能。

    按需加载的代码实现这里就不详细展开了,因为鉴于用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个Promise,当Promise成功以后执行回调。

    • Scope Hoisting

    Scope Hoisting 会分析出模板之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。

    如果在webpack4中你希望开启这个功能,只需要启用optimization.concatenateMudules 就可以了

module.exports = {
  optimization: {
    concatenateModules: true
  }
}

Tree Shaking

Tree Shaking 可以实现删除项目中未被引用的代码,比如:

// test.js
export const a = 1
export const b = 2
// index.js
import { a } from './test.js'

对于以上情况, test 文件中的变量b如果没有在项目中使用到的话,就不会被打包到文件中。如果使用Webpack4的话,开启生产环境就会自动启动这个优化功能。

  1. 如何用webpack来优化前端性能?

    用webpack优化前端性能是值优化webpack的输出结果,让打包的最终结果在浏览器运行快速高校

    • 压缩代码:删除多余的代码、注释、简化代码的代码的写法等等方式。可以利用css-loader?minimize 来压缩css
    • 利用CDN加速:在构建过程中,将引用的静态资源路径修改为CDN上对应的路径。可以利用webpack对应output参数和各loader的publicPath参数来修改资源路径
    • Tree Shaking:将代码中永远不会走到的片段删除掉。可以通过在启动webpack时追加参数 --optimize-minimize 来实现
    • Code Splitting:将代码按路由维度或者组件分块chunk,这样做到按需加载,同时可以充分利用浏览器缓存
    • 提取公共第三方库:SplitChunksPlugin插件来进行公共模块抽取,利用浏览器缓存可以长期缓存这些无需频繁变动的公共代码
  2. 如何提高webpack的构建速度

    • 多入口情况下,使用CommonsChunkPlugin来提取公共代码
    • 通过externals配置来提取常用库
    • 利用DIIPlugIn和DIIReferencePlugin预编译资源模块 通过DIIPlugin来对那些我们引用但是绝对不会修改的npm包来进行预编译,再通过DIIReferencePlugin将预编译的模块加载尽量。
    • 使用Happypack实现多线程加速编译
    • 使用webpack-uglify-parallel来提示uglifyPlugin的压缩速度。原理上webpack-uglify-parallel采用了多核并行压缩来提示压缩速度
    • 使用Tree-shaking和Scope Hoisting来剔除多余代码