几种常见的基础前端优化方式

in 网站建设
关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9
这篇文章算是最近学习前端优化时的一点心得,为了对比强烈降低了CPU性能,文中代码在github上也有一部分。
本文性能截图来自chrome自带的performance,不了解的可以看看跟他差不多的前世chrome timeline(介绍传送门)。

基础

CSS选择符优化

众所周知,css选择符解析顺序为从右向左,所以#id div的解析速度就不如div #id

减少回流重绘

浏览器渲染大致流程是这样的:

  1. 处理HTML标记并构造DOM树。
  2. 处理CSS标记并构造CSS规则树。
  3. 将DOM树与CSS规则树合并成一个渲染树。
  4. 根据渲染树来布局,以计算每个节点的几何信息。
  5. 将各个节点绘制到屏幕上。

当 Render Tree 中部分或全部, 因元素的尺寸、布局、隐藏等改变而需要重新构建,浏览器重新渲染的过程称为回流。
会导致回流的操作:

一些常用且会导致回流的属性和方法。

当页面中元素样式的改变并不影响布局时(像colorbackground-color等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。回流必将引起重绘,重绘不一定会引起回流。

任务切片

学名task-slice,算是一个必备的优化方式了,着重说一哈,先来看吃炸鸡的例子,为了突出优化前后差异把要吃的炸鸡变成1000份。
实验1:一份一份吃 吃1000次
image.png
实验2:一次吃1000份
image.png
实验3:分10次,每次吃100份
image.png
可以看到黄条代表的scripting从一段变成了好几段,对应的task也从一长条分了好几份。前文中缓存layout属性的部分讲过,浏览器会维护一个队列,所以实验1和实验2结果差距不大是因为他们都被放进队列中最后统一处理,而task-slice做的就是把一个long task,分成几个小task交给浏览器依次处理,缓解浏览器短时间内的压力。帧数也从2提升到了10+。(因为我测试时阉割了性能,所以优化后帧数依然感人)

上面这个例子,是同步的任务切片,那万一可爱的项目经理说要加10个echarts图咋办嘞。
其实同步和异步差不多的,上一个简单版本的代码

function TaskSlice(num, fn) {
  this.init(num, fn)
}
TaskSlice.prototype = {
  init: (num, fn) => {
    let index = 0
    function next() {
      if(index < num) {
        fn.call(this, index, next)
      }
      index++
    }
    next()
  }
}

使用的时候就这样

function drawCharts (num) {
  new TaskSlice(
    num,
    drawChart
  )
}

function drawChart(id, cb) {
  var chart = echarts.init(document.getElementById(id))
  chart.on('finished', cb)
  chart.on('finished', () => {
    chart.off()
  })
  chart.setOption(options)
}

因为echarts的生命周期是自己内部定义的事件,所以看起来比较麻烦,如果想要切片的异步任务是promise就比较简单了

function asyncTask(cb) {
  promise().then(() => {
    // balabalaba
    cb()
  })
}

这个类的逻辑大概是这样的:
初始化时传入要切片的次数num和异步的任务fn;
然后定义一个函数next,next通过闭包维护一个表示当前执行任务次数的变量index,然后调用next进入函数内逻辑;
判断执行次数是否小于要切的次数,小于的话,调用fn,同时给他两个参数分别为当前执行次数和next
然后进入fn函数,这里只需要在异步完成后调用next,任务就被切成了好多片。

减少作用域查找

作用域链和原型链类似,当我们使用对象的某一个属性时,会遍历原型链,从当前对象的属性开始,如果找不到该属性,则会在原型链上往下一个位置移动,寻找对应的属性,直到找到属性或者达到原型链末端。
在作用域链中,首先会在当前作用域中寻找我们需要的变量或者函数,如果没找到的话,则往上一个作用域寻找,直到找到变量/函数或者到达全局作用域。

//bad
var a=1;
function fn(){
  console.log(a);
}
fn()

//good
var a=1;
function fn(value){
  console.log(value);
}
fn(a)
节流防抖

throttle&debounce,这个网上文章太多了,而且像lodash这种工具库也有现成的源码,我也写了一个简版的,可能更通俗一点,就在文章开头说的github里,需要注意的是他们不能减少事件的触发次数。学就完事儿了。

懒加载

先将img标签中的src链接设为同一张图片,将其真正的图片地址存储在img标签的自定义属性。当js监听到该图片元素进入可视窗口时,再把src的值替换为自定义属性,减少首屏加载的请求数量,达到懒加载的效果。
其中的定义滚动事件,和计算是否进入可视窗口,就用到了前面说的防抖和缓存layout属性

let pending = false

function LazyLoad({ els, lazyDistance }) {
  this.lazyDistance = lazyDistance
  this.imglist = Array.from(els)
  this.loadedLen = 0
  this.init()
}
LazyLoad.prototype = {
  init: function() {
    this.initHandler()
    this.lazyLoad()
  },

  load: function(el) {
    if(!el.loaded) {
      el.src = el.getAttribute('data-src')
      this.loadedLen++
      el.loaded = true
    }
  },

  lazyLoad: function() {
    for(let i = 0; i < this.imglist.length; i++) {
      this.getBound(this.imglist[i]) && this.load(this.imglist[i])
    }
    pending = false
  },

  getBound: function(el) {
    let bound = el.getBoundingClientRect()
    let clientHeight = document.documentElement.clientHeight || document.body.clientHeight
    return bound.top <= clientHeight + this.lazyDistance
  },

  initHandler: function() {
    const fn = throttle(function() {
      if(!pending) {
        pending = true
        if(this.imglist.length > this.loadedLen) {
          this.lazyLoad()
        } else {
          window.removeEventListener('scroll', this.scrollHander, false)
        }
      }
    }, 1000)
    this.scrollHander = fn.bind(this)

    window.addEventListener('scroll', this.scrollHander, false)
  },
}

Vue

函数式组件

可以把没有状态,没有this上下文,没有生命周期的组件,写为函数式组件,因为函数式组件只是函数,所以渲染开销也低很多。具体写法官网传送门

拆分子组件

因为vue的渲染顺序为先父到子,所以拆分子组件类似上面所说的task slice。就是把一个大的task分成了父和子两个task。

使用v-show复用dom

下面这段话抄自官网
v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。
v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。

使用keep-alive进行缓存

keep-alive是Vue内置组件,会缓存该组件内的组件的实例,节省再次渲染时初始化组件的花销。

延迟加载DOM

这一项其实还是任务切片,但是这种实现方式真的和Vue特别契合,直接上代码

export default function (count = 10) {
  return {
    data () {
      return {
        displayPriority: 0,
      }
    },

    mounted () {
      this.runDisplayPriority()
    },

    methods: {
      runDisplayPriority () {
        const step = () => {
          requestAnimationFrame(() => {
            this.displayPriority++
            if (this.displayPriority < count) {
              step()
            }
          })
        }
        step()
      },

      defer (priority) {
        return this.displayPriority >= priority
      },
    },
  }
}

函数返回一个mixin,通过defer函数和v-if来控制切片,像这样:
image.png

不响应式数据

webpack

缩小文件搜索范围
压缩代码

浏览器从服务器访问网页时获取的 JavaScript、CSS 资源都是文本形式的,文件越大网页加载时间越长。 为了提升网页加速速度和减少网络传输流量,可以对这些资源进行压缩。js可以使用webpack内置的uglifyjs-webpack-plugin插件,css可以使用optimize-css-assets-webpack-plugin

optimization: {
  minimizer: [
    new UglifyJsPlugin(),
    new OptimizeCSSAssetsPlugin()
  ]
}
DllPlugin

dll是动态链接库,在一个动态链接库中可以包含给其他模块调用的函数和数据。包含基础的第三方模块(如vue全家桶)的动态链接库只需要编译一次,之后的构建中这些模块就不需要重新编译,而是直接使用动态链接库中的代码。所以会大大提升构建速度。
具体操作是使用DllPluginDllReferencePlugin这两个内置的插件,前者用于打包出动态链接库文件,后者用于主webpack配置中去引用。

// 打包dll
entry: {
  vendor: ['vue', 'vue-router', 'vuex'],
},
output: {
  filename: '[name].dll.js',
  path: path.resolve(__dirname, 'dist'),
  library: '_dll_[name]',
},
plugins: [
  new DllPlugin({
    name: '_dll_[name]',
    path: path.join(__dirname, 'dist', '[name].manifest.json'),
  }),
],
// output和plugins中的[name]都是entry中的key,
// 也就是'vender'
// 引用
plugins: [
  new DllReferencePlugin({
    manifest: require('../dist/vendor.manifest.json'),
  }),
]
happypack

由于运行在Node.js之上的Webpack是单线程的,所以Webpack需要处理的任务会一件件挨着做,不能多个事情一起做。而HappyPack可以把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。不多bb上代码

const HappyPack = require('happypack')

module: {
  rules: [
    {
      test: /\.js$/,
      use: ['happypack/loader?id=babel']
    }
  ],
},
plugins: [
  new HappyPack({
    // 用唯一的标识符 id 来代表当前
    // 的 HappyPack 是用来处理一类特定的文件
    id: 'bable',
    loaders: ['babel-loader'],
  })
]

但是HappyPack(v5.0.1)并不支持vue-loader(v15.3.0)(支持列表),而在vue的项目中,使用模板语法的话大部分的业务js都是写在.vue文件中的,就可以通过配置vue-loader的options部分,将js部分交由happypack处理

好像之前的vue-loader是支持的,改成需要在pulgins里面单独声明之后就不行了,而vue-loader升级是加快了打包速度的,强行为了使用happypack而降级有点舍本逐末的味道。
//rules: [
//  {
//    test: /\.vue$/,
//    use: [
//      {
//        loader: 'vue-loader',
//        options: {
//          loaders: {
//            js: 'happypack/loader?id=babel'
//          },
//        }
//      }
//    ]
//  }
//]

不支持也没有关系,vue Loader文档有说,在pulgins中引用可以将你定义过的其它规则复制并应用到.vue文件里相应语言的块。例如,如果你有一条匹配/\.js$/的规则,那么它会应用到.vue文件里的<script>块。

其他

WebAssembly

了解这个东西是看webpack文档的时候,发现resolve.extensions的默认配置是['.wasm', '.mjs', '.js', '.json'],这个wasm甚至是排在第一位的,就去了解了一下,真是不看不知道一看吓一跳,这玩意儿也忒厉害咧,我的理解浏览器识别js代码的大概流程是下载->转换->编译,但是wasm可以跳过转换和编译两步,因为他本身就可以被浏览器识别,从而而且最近WebAssembly也正式加入到W3C标准了,别问,问就是知识点。放一个[mdn对于WebAssembly的介绍]当作拓展阅读(https://developer.mozilla.org...

关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9
扫一扫关注公众号添加购物返利助手,领红包
Comments are closed.

推荐使用阿里云服务器

超多优惠券

服务器最低一折,一年不到100!

朕已阅去看看