前端性能优化

99 阅读9分钟

CDN

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

CDN一般会用来托管Web资源(包括文本、图片和脚本等),可供下载的资源(媒体文件、软件、文档等),应用程序(门户网站等)。使用CDN来加速这些资源的访问。

在性能方面,引入CDN的作用在于:

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

懒加载

懒加载也叫做延迟加载、按需加载,指的是在长网页中延迟加载图片数据,是一种较好的网页性能优化的方式。在比较长的网页或应用中,如果图片很多,所有的图片都被加载出来,而用户只能看到可视窗口的那一部分图片数据,这样就浪费了性能。

懒加载的特点

  • 减少无用资源的加载:使用懒加载明显减少了服务器的压力和流量,同时也减小了浏览器的负担。
  • 提升用户体验: 如果同时加载较多图片,可能需要等待的时间较长,这样影响了用户体验,而使用懒加载就能大大的提高用户体验。
  • 防止加载过多图片而影响其他资源文件的加载 :会影响网站应用的正常使用。

懒加载的实现原理

图片的加载是由src引起的,当对src赋值时,浏览器就会请求图片资源。根据这个原理,我们使用HTML5 的data-xxx属性来储存图片的路径,在需要加载图片的时候,将data-xxx中图片的路径赋值给src,这样就实现了图片的按需加载,即懒加载。

window.innerHeight 是浏览器可视区的高度

(2)document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动的过的距离

(3)imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离)

(4)图片加载条件:img.offsetTop < window.innerHeight + document.body.scrollTop;

<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');
function lozyLoad(){
		var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
		var winHeight= window.innerHeight;
		for(var i=0;i < imgs.length;i++){
			if(imgs[i].offsetTop < scrollTop + winHeight ){
				imgs[i].src = imgs[i].getAttribute('data-src');
			}
		}
	}
  window.onscroll = lozyLoad();
</script>

IntersectionObserver

const imgs = document.querySelectorAll('img') //获取所有待观察的目标元素
var options = {}
function lazyLoad(target) {
  const observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entrie => {
      if (entrie.isIntersecting) {
        const img = entrie.target;
        const src = img.getAttribute('data-src');
        img.setAttribute('src', src)
        observer.unobserve(img); // 停止监听已开始加载的图片
      }

    })
  }, options);
  observer.observe(target)
}

imgs.forEach(lazyLoad)

getBoundingClientRect

 <img src="placeholder.jpg" data-src="image.jpg" alt="Image">
 <script>
 window.addEventListener('DOMContentLoaded', () => {
   const images = document.querySelectorAll('img[data-src]');
   const viewportHeight = window.innerHeight;
   const loadImages = () => {
     for (let i = 0; i < images.length; i++) {
       const rect = images[i].getBoundingClientRect();
       if (rect.top < viewportHeight) {
         images[i].src = images[i].getAttribute('data-src');
         images[i].removeAttribute('data-src');
       }
     }
   };
   window.addEventListener('scroll', loadImages);
   window.addEventListener('resize', loadImages);
   loadImages();
 });
 </script>

减少回流与重绘

  • 操作DOM时,尽量在低层级的DOM节点进行操作

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

  • 使用CSS的表达式

  • 不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式。

  • 使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素

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

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

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

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

防抖节流

// 防抖
function debounce(fn, wait) {
  var timer = null;

  return function() {
    var context = this,
      args = [...arguments];

    // 如果此时存在定时器的话,则取消之前的定时器重新记时
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    // 设置定时器,使事件间隔指定事件后执行
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  };
}

// 节流时间戳版
function throttle(fn, delay) {
  var preTime = Date.now();

  return function() {
    var context = this,
      args = [...arguments],
      nowTime = Date.now();

    // 如果两次时间间隔超过了指定时间,则执行函数。
    if (nowTime - preTime >= delay) {
      preTime = Date.now();
      return fn.apply(context, args);
    }
  };
}

// 节流定时器版
function throttle (fun, wait){
  let timeout = null
  return function(){
    let context = this
    let args = [...arguments]
    if(!timeout){
      timeout = setTimeout(() => {
        fun.apply(context, args)
        timeout = null 
      }, wait)
    }
  }
}

启用前端缓存

前端缓存,其实就是http缓存,通过(强缓存/协商缓存)等方式让计算机直接从缓存中读取静态资源,从而实现节约宽带提高响应速度减少服务器压力等优化。

异步加载script文件或者放在最后加载

浏览器渲染主要有以下步骤:

  • 首先解析收到的文档,根据文档定义构建一棵 DOM 树,DOM 树是由 DOM 元素及属性节点组成的。
  • 然后对 CSS 进行解析,生成 CSSOM 规则树。
  • 根据 DOM 树和 CSSOM 规则树构建渲染树。渲染树的节点被称为渲染对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和 DOM 元素相对应,但这种对应关系不是一对一的,不可见的 DOM 元素不会被插入渲染树。还有一些 DOM元素对应几个可见对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。
  • 当渲染对象被创建并添加到树中,它们并没有位置和大小,所以当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。
  • 布局阶段结束后是绘制阶段,遍历渲染树并调用渲染对象的 paint 方法将它们的内容显示在屏幕上,绘制使用 UI 基础组件。

image.png JavaScript既会阻塞HTML的解析,也会阻塞CSS的解析。因此我们可以对JavaScript的加载方式进行改变,来进行优化: 尽量将JavaScript文件放在body的最后

(2) body中间尽量不要写<script>标签

(3)<script>标签的引入资源方式有三种,有一种就是我们常用的直接引入,还有两种就是使用 async 属性和 defer 属性来异步引入,两者都是去异步加载外部的JS文件,不会阻塞DOM的解析(尽量使用异步加载)。三者的区别如下:

  • script 立即停止页面渲染去加载资源文件,当资源加载完毕后立即执行js代码,js代码执行完毕后继续渲染页面;
  • async 是在下载完成之后,立即异步加载,加载好后立即执行,多个带async属性的标签,不能保证加载的顺序;
  • defer 是在下载完成之后,立即异步加载。加载好后,如果 DOM 树还没构建好,则先等 DOM 树解析好再执行;如果DOM树已经准备好,则立即执行。多个带defer属性的标签,按照顺序执行。

将图片替换为webp格式

webp格式的图片比png/jpg有着更优秀的算法。在图片体积上会比jpg/png更小。所以加载的也就更快,耗费的带宽也就越少。占用加载资源的时间也就越短。

合并请求

事件委托

利用冒泡机制,将原本应该绑定在子元素上的事件全部交由父元素来完成的行为被称为事件委托。 事件委托可以减少内存消耗和DOM操作

尽量使用CSS来完成动画

尽量避免使用JS完成动画,js会占用主线程

尝试使用Web Worker

多线程,处理耗时长的任务

使用Http2.0

多路复用,二进制分帧,头部压缩

使用骨架屏

用css提前占好位置,当资源加载完成即可填充,减少页面的回流与重绘,同时还能给用户最直接的反馈。

Webpack打包优化

优化loader的搜索范围

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

将 babel 编译过的文件缓存起来

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,  // js 文件才使用 babel
        use: {
          loader: 'babel-loader',
          options: {
            cacheDirectory: true
          }
        },
      }
    ]
  }
}

IgnorePlugin:避免引入无用模块

// index.js
import 'moment/locale/zh-cn';

// webpack.prod.js
module.exports = {
  plugin: [
    new webpack.IgnorePlugin({  // 忽略 moment 下的 /locale 目录
      resourceRegExp: /^\.\/locale$/,
      contextRegExp: /moment$/,
    }),
  ]
}

noParse:避免重复打包

module.exports = {
  module: {
    noParse: [/react\/.min\.js$/]
  }
}

HappyPack:多进程打包

// webpack.prod.js
const HappyPack = require('happypack');
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ["happyPack/loader?id=babel"],  // 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
        include: srcPath,
      },
    ]
  },
  plugins: [
    // happyPack 开启多进程打包
    new HappyPack({
      id: "babel",  // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
      loaders: ["babel-loader?cacheDirectory"],   // 如何处理 .js 文件,用法和 Loader 配置中一样
    }),
  ]
}

ParallelUglifyPlugin:多进程压缩 JS

// webpack.prod.js
module.exports = {
  plugins: [
    // 使用 ParallelUglifyPlugin 并行压缩输出的 js 代码
    new ParallelUglifyPlugin({
      // 传递给 UglifyJS 的参数
      // 还是使用 UglifyJS 压缩,只不过帮助开启了多进程
      uglifyJS: {
        output: {
          beautify: false, // 最紧凑的输出
          comments: false, // 删除所有的注释
        },
        compress: {
          drop_console: true,  // 删除所有的 console 语句,可以兼容 IE 浏览器
          collapse_vars: true,  // 内嵌定义了但是只用过一次的变量
          reduce_vars: true,  // 提取出出现多次,但是没有被定义成变量去引用的静态值
        },
      },
    }),
  ]
}
/**
  * var a = 10;
  * var b = 20;
  * var c = a + b;
  * 会被编译成
  * var c = 30;
  */

代码分割

// webpack.prod.js
module.exports = {
  entry: {
    index: path.join(srcPath, 'index.js'),
    other: path.join(srcPath, 'other.js')
  },
  plugins: [
    // 多入口 —— 生成 index.html
    new HtmlWebpackPlugin({
      template: path.join(srcPath, 'index.html'),
      filename: 'index.html',
      // chunks 表示该页面需要引用哪些 chunk
      chunks: ['index', 'vendor', 'common'] // 考虑代码分割
    }),
    // 多入口 —— 生成 other.html
    new HtmlWebpackPlugin({
      template: path.join(srcPath, 'other.html'),
      filename: 'other.html',
      chunks: ['other', 'vendor', 'common']
    }),
  ],
  optimization: {
    // 分割代码块
    splitChunks: {
      /**
       * initial: 入口 chunks,对于异步导入的文件不处理
       * async: 异步 chunk,只对异步导入的文件处理
       * all: 全部 chunk
       */
      chunks: 'all',

      // 缓存分组
      cacheGroups: {
        // 第三方模块
        vendor: {
          name: 'vendor', // chunk 名称
          priority: 1, // 权限更高,优先抽离,重要!!
          test: /node_modules/,
          minSize: 0, // 大小限制
          minChunks: 1, // 最少复用几次
        },

        // 公共的模块
        common: {
          name: 'common', // chunk 名称
          priority: 0, // 优先级
          minSize: 0, // 公共模块的大小限制
          minChunks: 2 // 公共模块最少复用过几次
        }
      }
    }
  }
}

bundle 加 hash

// webpack.config.js
module.exports = {
   output: {
      path: path.resolve(__dirname, "./dist"),
      filename: "[name].[hash].js",
      clean: true,
    }
}

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

const ModuleConcatenationPlugin = require('webpack/lib/ModuleConcatenationPlugin');

module.exports = {
  resolve: {
    // 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向 ES6 模块化语法的文件
    mainFields: ['jsnext:main', 'browser', 'main']
  },
  plugins: [
    // 开启 Scope Hoisting
    new ModuleConcatenationPlugin()
  ]
}

抽离压缩 CSS 文件

// webpack.dev.js
module.exports = {
  rules: [
    {
      test: /\.css$/,
      // loader 的执行顺序是从后往前,postcss-loader 是处理浏览器兼容性问题的
      use: ['style-loader', 'css-loader', 'postcss-loader']
    },
    {
      test: /\.less$/,
      // 增加 less-loader,注意顺序
      use: ['style-loader', 'css-loader', 'less-loader']
    }
  ],
}

// webpack.prod.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const TerserJSPlugin = require("terser-webpack-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");

module.exports = {
  rules: [
    // 抽离 css
    {
      test: /\.css$/,
      use: [
        MiniCssExtractPlugin.loader,
        'css-loader',
        'postcss-loader',
      ]
    },
    // 抽离 less
    {
      test: /\.less$/,
      use: [
        MiniCssExtractPlugin.loader,
        'css-loader',
        'less-loader',
        'postcss-loader'
      ]
    }
  ],
  
  plugins: [
    // 抽离 css 文件
    new MiniCssExtractPlugin({
      filename: 'css/main.[contenthash:8].css'
    })
  ],
  
  optimization: {
    // 压缩 css
    minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugiin({})]
  }
}