精华内容
下载资源
问答
  • vue开发的网站seo优化的方法

    千次阅读 2021-02-19 15:06:45
    vue做seo优化的终极解决方案,也是服务端渲染。我们可以使用vue的服务端渲染框架来开发网站,然后发布成服务端渲染的网站就行了。 vue的服务端渲染框架,最多人用的框架应该是minissr。我们可以到这去获取: ...

    由于vue是通过js来渲染html的内容的,但是搜索引擎对js并不友好,搜索引擎并不会自动像浏览器那样帮我们抓取网站的真正内容。

    因此,我们就需要像传统的php、ssh或.net网站那样,需要一个服务端渲染的网站。

    vue做seo优化的终极解决方案,也是服务端渲染。我们可以使用vue的服务端渲染框架来开发网站,然后发布成服务端渲染的网站就行了。

    vue的服务端渲染框架,最多人用的框架应该是minissr。我们可以到这去获取:

    https://www.wechatmini.com/vue/minissrdetail

    这个框架使用起来非常简单,获取到它的代码后,通过一个命令就可以部署测试了:

    npm install
    npm run ssr

    在这个框架上开发也跟普通的vue开发没什么区别,都是使用vue+vuex模式的开发。

    我们通过127.0.0.1:3004就可以看到效果:

     

    打开源代码看效果:

     

    它已经帮我们配置好所有的东西,包括路由等等。

    我们seo选要的多页面、title、keywords和html内容,这些全部都是服务端渲染输出,完美!

    展开全文
  • 我们在使用 Vue 或其他框架的日常开发中,或多或少的都会遇到一些性能问题,尽管 Vue 内部已经帮助我们了许多优化,但是还是有些问题是需要我们主动去避免的。我在我的日常开中,以及网上各种大佬的文章中总结了...

    前言

    我们在使用 Vue 或其他框架的日常开发中,或多或少的都会遇到一些性能问题,尽管 Vue 内部已经帮助我们做了许多优化,但是还是有些问题是需要我们主动去避免的。我在我的日常开中,以及网上各种大佬的文章中总结了一些容易产生性能问题的场景以及针对这些问题优化的技巧,这篇文章就来探讨下,希望对你有所帮助。

    一、使用v-slot:slotName,而不是slot="slotName"

    v-slot是 2.6 新增的语法,具体可查看:Vue2.6,2.6 发布已经是快两年前的事情了,但是现在仍然有不少人仍然在使用slot="slotName"这个语法。虽然这两个语法都能达到相同的效果,但是内部的逻辑确实不一样的,下面来看下这两种方式有什么不同之处。

    我们先来看下这两种语法分别会被编译成什么:

    使用新的写法,对于父组件中的以下模板:

    <child>
      <template v-slot:name>{{name}}</template>
    </child>
    

    会被编译成:

    function render() {
      with (this) {
        return _c('child', {
          scopedSlots: _u([
            {
              key: 'name',
              fn: function () {
                return [_v(_s(name))]
              },
              proxy: true
            }
          ])
        })
      }
    }
    

    使用旧的写法,对于以下模板:

    <child>
      <template slot="name">{{name}}</template>
    </child>
    复制代码
    

    会被编译成:

    function render() {
      with (this) {
        return _c(
          'child',
          [
            _c(
              'template',
              {
                slot: 'name'
              },
              [_v(_s(name))]
            )
          ],
        )
      }
    }
    复制代码
    

    通过编译后的代码可以发现,旧的写法是将插槽内容作为 children 渲染的,会在父组件的渲染函数中创建,插槽内容的依赖会被父组件收集(name 的 dep 收集到父组件的渲染 watcher),而新的写法将插槽内容放在了 scopedSlots 中,会在子组件的渲染函数中调用,插槽内容的依赖会被子组件收集(name 的 dep 收集到子组件的渲染 watcher),最终导致的结果就是:当我们修改 name 这个属性时,旧的写法是调用父组件的更新(调用父组件的渲染 watcher),然后在父组件更新过程中调用子组件更新(prePatch => updateChildComponent),而新的写法则是直接调用子组件的更新(调用子组件的渲染 watcher)。

    这样一来,旧的写法在更新时就多了一个父组件更新的过程,而新的写法由于直接更新子组件,就会更加高效,性能更好,所以推荐始终使用v-slot:slotName语法。

    二、使用计算属性

    这一点已经被提及很多次了,计算属性最大的一个特点就是它是可以被缓存的,这个缓存指的是只要它的依赖的不发生改变,它就不会被重新求值,再次访问时会直接拿到缓存的值,在做一些复杂的计算时,可以极大提升性能。可以看以下代码:

    <template>
      <div>{{superCount}}</div>
    </template>
    <script>
      export default {
        data() {
          return {
            count: 1
          }
        },
        computed: {
          superCount() {
            let superCount = this.count
            // 假设这里有个复杂的计算
            for (let i = 0; i < 10000; i++) {
              superCount++
            }
            return superCount
          }
        }
      }
    </script>
    复制代码
    

    这个例子中,在 created、mounted 以及模板中都访问了 superCount 属性,这三次访问中,实际上只有第一次即created时才会对 superCount 求值,由于 count 属性并未改变,其余两次都是直接返回缓存的 value,对于计算属性更加详细的介绍可以看我之前写的文章:Vue computed 是如何实现的?。

    三、使用函数式组件

    对于某些组件,如果我们只是用来显示一些数据,不需要管理状态,监听数据等,那么就可以用函数式组件。函数式组件是无状态的,无实例的,在初始化时不需要初始化状态,不需要创建实例,也不需要去处理生命周期等,相比有状态组件,会更加轻量,同时性能也更好。具体的函数式组件使用方式可参考官方文档:函数式组件

    我们可以写一个简单的 demo 来验证下这个优化:

    // UserProfile.vue
    <template>
      <div class="user-profile">{{ name }}</div>
    </template>
    
    <script>
      export default {
        props: ['name'],
        data() {
          return {}
        },
        methods: {}
      }
    </script>
    <style scoped></style>
    
    // App.vue
    <template>
      <div id="app">
        <UserProfile v-for="item in list" :key="item" : />
      </div>
    </template>
    
    <script>
      import UserProfile from './components/UserProfile'
    
      export default {
        name: 'App',
        components: { UserProfile },
        data() {
          return {
            list: Array(500)
              .fill(null)
              .map((_, idx) => 'Test' + idx)
          }
        },
        beforeMount() {
          this.start = Date.now()
        },
        mounted() {
          console.log('用时:', Date.now() - this.start)
        }
      }
    </script>
    
    <style></style>
    复制代码
    

    UserProfile 这个组件只渲染了 props 的 name,然后在 App.vue 中调用 500 次,统计从 beforeMount 到 mounted 的耗时,即为 500 个子组件(UserProfile)初始化的耗时。

    经过我多次尝试后,发现耗时一直在 30ms 左右,那么现在我们再把改成 UserProfile 改成函数式组件:

    <template functional>
      <div class="user-profile">{{ props.name }}</div>
    </template>
    复制代码
    

    此时再经过多次尝试后,初始化的耗时一直在 10-15ms,这些足以说明函数式组件比有状态组件有着更好的性能。

    四、结合场景使用 v-show 和 v-if

    以下是两个使用 v-show 和 v-if 的模板

    <template>
      <div>
        <UserProfile :user="user1" v-if="visible" />
        <button @click="visible = !visible">toggle</button>
      </div>
    </template>
    复制代码
    
    <template>
      <div>
        <UserProfile :user="user1" v-show="visible" />
        <button @click="visible = !visible">toggle</button>
      </div>
    </template>
    复制代码
    

    这两者的作用都是用来控制某些组件或 DOM 的显示 / 隐藏,在讨论它们的性能差异之前,先来分析下这两者有何不同。其中,v-if 的模板会被编译成:

    function render() {
      with (this) {
        return _c(
          'div',
          [
            visible
              ? _c('UserProfile', {
                  attrs: {
                    user: user1
                  }
                })
              : _e(),
            _c(
              'button',
              {
                on: {
                  click: function ($event) {
                    visible = !visible
                  }
                }
              },
              [_v('toggle')]
            )
          ],
        )
      }
    }
    复制代码
    

    可以看到,v-if 的部分被转换成了一个三元表达式,visible 为 true 时,创建一个 UserProfile 的 vnode,否则创建一个空 vnode,在 patch 的时候,新旧节点不一样,就会移除旧的节点或创建新的节点,这样的话UserProfile也会跟着创建 / 销毁。如果UserProfile组件里有很多 DOM,或者要执行很多初始化 / 销毁逻辑,那么随着 visible 的切换,势必会浪费掉很多性能。这个时候就可以用 v-show 进行优化,我们来看下 v-show 编译后的代码:

    function render() {
      with (this) {
        return _c(
          'div',
          [
            _c('UserProfile', {
              directives: [
                {
                  name: 'show',
                  rawName: 'v-show',
                  value: visible,
                  expression: 'visible'
                }
              ],
              attrs: {
                user: user1
              }
            }),
            _c(
              'button',
              {
                on: {
                  click: function ($event) {
                    visible = !visible
                  }
                }
              },
              [_v('toggle')]
            )
          ],
        )
      }
    }
    复制代码
    

    v-show被编译成了directives,实际上,v-show 是一个 Vue 内部的指令,在这个指令的代码中,主要执行了以下逻辑:

    el.style.display = value ? el.__vOriginalDisplay : 'none'
    复制代码
    

    它其实是通过切换元素的 display 属性来控制的,和 v-if 相比,不需要在 patch 阶段创建 / 移除节点,只是根据v-show上绑定的值来控制 DOM 元素的style.display属性,在频繁切换的场景下就可以节省很多性能。

    但是并不是说v-show可以在任何情况下都替换v-if,如果初始值是false时,v-if并不会创建隐藏的节点,但是v-show会创建,并通过设置style.display='none'来隐藏,虽然外表看上去这个 DOM 都是被隐藏的,但是v-show已经完整的走了一遍创建的流程,造成了性能的浪费。

    所以,v-if的优势体现在初始化时,v-show体现在更新时,当然并不是要求你绝对按照这个方式来,比如某些组件初始化时会请求数据,而你想先隐藏组件,然后在显示时能立刻看到数据,这时候就可以用v-show,又或者你想每次显示这个组件时都是最新的数据,那么你就可以用v-if,所以我们要结合具体业务场景去选一个合适的方式。

    五、使用 keep-alive

    在动态组件的场景下:

    <template>
      <div>
        <component :is="currentComponent" />
      </div>
    </template>
    复制代码
    

    这个时候有多个组件来回切换,currentComponent每变一次,相关的组件就会销毁 / 创建一次,如果这些组件比较复杂的话,就会造成一定的性能压力,其实我们可以使用 keep-alive 将这些组件缓存起来:

    <template>
      <div>
        <keep-alive>
          <component :is="currentComponent" />
        </keep-alive>
      </div>
    </template>
    复制代码
    

    keep-alive的作用就是将它包裹的组件在第一次渲染后就缓存起来,下次需要时就直接从缓存里面取,避免了不必要的性能浪费,在讨论上个问题时,说的是v-show初始时性能压力大,因为它要创建所有的组件,其实可以用keep-alive优化下:

    <template>
      <div>
        <keep-alive>
          <UserProfileA v-if="visible" />
          <UserProfileB v-else />
        </keep-alive>
      </div>
    </template>
    复制代码
    

    这样的话,初始化时不会渲染UserProfileB组件,当切换visible时,才会渲染UserProfileB组件,同时被keep-alive缓存下来,频繁切换时,由于是直接从缓存中取,所以会节省很多性能,所以这种方式在初始化和更新时都有较好的性能。

    但是keep-alive并不是没有缺点,组件被缓存时会占用内存,属于空间和时间上的取舍,在实际开发中要根据场景选择合适的方式。

    六、避免 v-for 和 v-if 同时使用

    这一点是 Vue 官方的风格指南中明确指出的一点:Vue 风格指南

    如以下模板:

    <ul>
      <li v-for="user in users" v-if="user.isActive" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
    复制代码
    

    会被编译成:

    // 简化版
    function render() {
      return _c(
        'ul',
        this.users.map((user) => {
          return user.isActive
            ? _c(
                'li',
                {
                  key: user.id
                },
                [_v(_s(user.name))]
              )
            : _e()
        }),
      )
    }
    复制代码
    

    可以看到,这里是先遍历(v-for),再判断(v-if),这里有个问题就是:如果你有一万条数据,其中只有 100 条是isActive状态的,你只希望显示这 100 条,但是实际在渲染时,每一次渲染,这一万条数据都会被遍历一遍。比如你在这个组件内的其他地方改变了某个响应式数据时,会触发重新渲染,调用渲染函数,调用渲染函数时,就会执行到上面的代码,从而将这一万条数据遍历一遍,即使你的users没有发生任何改变。

    为了避免这个问题,在此场景下你可以用计算属性代替:

    <template>
      <div>
        <ul>
          <li v-for="user in activeUsers" :key="user.id">{{ user.name }}</li>
        </ul>
      </div>
    </template>
    
    <script>
      export default {
        // ...
        computed: {
          activeUsers() {
            return this.users.filter((user) => user.isActive)
          }
        }
      }
    </script>
    复制代码
    

    这样只会在users发生改变时才会执行这段遍历的逻辑,和之前相比,避免了不必要的性能浪费。

    七、始终为 v-for 添加 key,并且不要将 index 作为的 key

    这一点是 Vue 风格指南中明确指出的一点,同时也是面试时常问的一点,很多人都习惯的将 index 作为 key,这样其实是不太好的,index 作为 key 时,将会让 diff 算法产生错误的判断,从而带来一些性能问题,你可以看下 ssh 大佬的文章,深入分析下,为什么 Vue 中不要用 index 作为 key。在这里我也通过一个例子来简单说明下当 index 作为 key 时是如何影响性能的。

    看下这个例子:

    const Item = {
      name: 'Item',
      props: ['message', 'color'],
      render(h) {
        debugger
        console.log('执行了Item的render')
        return h('div', { style: { color: this.color } }, [this.message])
      }
    }
    
    new Vue({
      name: 'Parent',
      template: `
      <div @click="reverse" class="list">
        <Item
          v-for="(item,index) in list"
          :key="item.id"
          :message="item.message"
          :color="item.color"
        />
      </div>`,
      components: { Item },
      data() {
        return {
          list: [
            { id: 'a', color: '#f00', message: 'a' },
            { id: 'b', color: '#0f0', message: 'b' }
          ]
        }
      },
      methods: {
        reverse() {
          this.list.reverse()
        }
      }
    }).$mount('#app')
    复制代码
    

    这里有一个 list,会渲染出来a b,点击后会执行reverse方法将这个 list 颠倒下顺序,你可以将这个例子复制下来,在自己的电脑上看下效果。

    我们先来分析用id作为 key 时,点击时会发生什么,

    由于 list 发生了改变,会触发Parent组件的重新渲染,拿到新的vnode,和旧的vnode去执行patch,我们主要关心的就是patch过程中的updateChildren逻辑,updateChildren就是对新旧两个children执行diff算法,使尽可能地对节点进行复用,对于我们这个例子而言,此时旧的children是:

    ;[
      {
        tag: 'Item',
        key: 'a',
        propsData: {
          color: '#f00',
          message: '红色'
        }
      },
      {
        tag: 'Item',
        key: 'b',
        propsData: {
          color: '#0f0',
          message: '绿色'
        }
      }
    ]
    复制代码
    

    执行reverse后的新的children是:

    ;[
      {
        tag: 'Item',
        key: 'b',
        propsData: {
          color: '#0f0',
          message: '绿色'
        }
      },
      {
        tag: 'Item',
        key: 'a',
        propsData: {
          color: '#f00',
          message: '红色'
        }
      }
    ]
    复制代码
    

    此时执行updateChildrenupdateChildren会对新旧两组 children 节点的循环进行对比:

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 对新旧节点执行patchVnode
        // 移动指针
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 对新旧节点执行patchVnode
        // 移动指针
      } else if (sameVnode(oldStartVnode, newEndVnode)) {
        // 对新旧节点执行patchVnode
        // 移动oldStartVnode节点
        // 移动指针
      } else if (sameVnode(oldEndVnode, newStartVnode)) {
        // 对新旧节点执行patchVnode
        // 移动oldEndVnode节点
        // 移动指针
      } else {
        //...
      }
    }
    复制代码
    

    通过sameVnode判断两个节点是相同节点的话,就会执行相应的逻辑:

    function sameVnode(a, b) {
      return (
        a.key === b.key &&
        ((a.tag === b.tag &&
          a.isComment === b.isComment &&
          isDef(a.data) === isDef(b.data) &&
          sameInputType(a, b)) ||
          (isTrue(a.isAsyncPlaceholder) &&
            a.asyncFactory === b.asyncFactory &&
            isUndef(b.asyncFactory.error)))
      )
    }
    复制代码
    

    sameVnode主要就是通过 key 去判断,由于我们颠倒了 list 的顺序,所以第一轮对比中:sameVnode(oldStartVnode, newEndVnode)成立,即旧的首节点和新的尾节点是同一个节点,此时会执行patchVnode逻辑,patchVnode中会执行prePatchprePatch中会更新 props,此时我们的两个节点的propsData是相同的,都为{color: '#0f0',message: '绿色'},这样的话Item组件的 props 就不会更新,Item也不会重新渲染。再回到updateChildren中,会继续执行"移动oldStartVnode节点"的操作,将 DOM 元素。移动到正确位置,其他节点对比也是同样的流程。

    可以发现,在整个流程中,只是移动了节点,并没有触发 Item 组件的重新渲染,这样实现了节点的复用。

    我们再来看下使用index作为 key 的情况,使用index时,旧的children是:

    ;[
      {
        tag: 'Item',
        key: 0,
        propsData: {
          color: '#f00',
          message: '红色'
        }
      },
      {
        tag: 'Item',
        key: 1,
        propsData: {
          color: '#0f0',
          message: '绿色'
        }
      }
    ]
    复制代码
    

    执行reverse后的新的children是:

    ;[
      {
        tag: 'Item',
        key: 0,
        propsData: {
          color: '#0f0',
          message: '绿色'
        }
      },
      {
        tag: 'Item',
        key: 1,
        propsData: {
          color: '#f00',
          message: '红色'
        }
      }
    ]
    复制代码
    

    这里和id作为 key 时的节点就有所不同了,虽然我们把 list 顺序颠倒了,但是 key 的顺序却没变,在updateChildrensameVnode(oldStartVnode, newStartVnode)将会成立,即旧的首节点和新的首节点相同,此时执行patchVnode -> prePatch -> 更新props,这个时候旧的 propsData 是{color: '#f00',message: '红色'},新的 propsData 是{color: '#0f0',message: '绿色'},更新过后,Item 的 props 将会发生改变,会触发 Item 组件的重新渲染

    这就是 index 作为 key 和 id 作为 key 时的区别,id 作为 key 时,仅仅是移动了节点,并没有触发 Item 的重新渲染。index 作为 key 时,触发了 Item 的重新渲染,可想而知,当 Item 是一个复杂的组件时,必然会引起性能问题。

    上面的流程比较复杂,涉及的也比较多,可以拆开写好几篇文章,有些地方我只是简略的说了一下,如果你不是很明白的话,你可以把上面的例子复制下来,在自己的电脑上调式,我在 Item 的渲染函数中加了打印日志和 debugger,你可以分别用 id 和 index 作为 key 尝试下,你会发现 id 作为 key 时,Item 的渲染函数没有执行,但是 index 作为 key 时,Item 的渲染函数执行了,这就是这两种方式的区别。

    八、延迟渲染

    延迟渲染就是分批渲染,假设我们某个页面里有一些组件在初始化时需要执行复杂的逻辑:

    <template>
      <div>
        <!-- Heavy组件初始化时需要执行很复杂的逻辑,执行大量计算 -->
        <Heavy1 />
        <Heavy2 />
        <Heavy3 />
        <Heavy4 />
      </div>
    </template>
    复制代码
    

    这将会占用很长时间,导致帧数下降、卡顿,其实可以使用分批渲染的方式来进行优化,就是先渲染一部分,再渲染另一部分:

    参考黄轶老师揭秘 Vue.js 九个性能优化技巧中的代码:

    <template>
      <div>
        <Heavy v-if="defer(1)" />
        <Heavy v-if="defer(2)" />
        <Heavy v-if="defer(3)" />
        <Heavy v-if="defer(4)" />
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          displayPriority: 0
        }
      },
      mounted() {
        this.runDisplayPriority()
      },
      methods: {
        runDisplayPriority() {
          const step = () => {
            requestAnimationFrame(() => {
              this.displayPriority++
              if (this.displayPriority < 10) {
                step()
              }
            })
          }
          step()
        },
        defer(priority) {
          return this.displayPriority >= priority
        }
      }
    }
    </script>
    
    复制代码
    

    其实原理很简单,主要是维护displayPriority变量,通过requestAnimationFrame在每一帧渲染时自增,然后我们就可以在组件上通过v-if="defer(n)"使displayPriority增加到某一值时再渲染,这样就可以避免 js 执行时间过长导致的卡顿问题了。

    九、使用非响应式数据

    在 Vue 组件初始化数据时,会递归遍历在 data 中定义的每一条数据,通过Object.defineProperty将数据改成响应式,这就意味着如果 data 中的数据量很大的话,在初始化时将会使用很长的时间去执行Object.defineProperty, 也就会带来性能问题,这个时候我们可以强制使数据变为非响应式,从而节省时间,看下这个例子:

    <template>
      <div>
        <ul>
          <li v-for="item in heavyData" :key="item.id">{{ item.name }}</li>
        </ul>
      </div>
    </template>
    
    <script>
    // 一万条数据
    const heavyData = Array(10000)
      .fill(null)
      .map((_, idx) => ({ name: 'test', message: 'test', id: idx }))
    
    export default {
      data() {
        return {
          heavyData: heavyData
        }
      },
      beforeCreate() {
        this.start = Date.now()
      },
      created() {
        console.log(Date.now() - this.start)
      }
    }
    </script>
    复制代码
    

    heavyData中有一万条数据,这里统计了下从beforeCreatecreated经历的时间,对于这个例子而言,这个时间基本上就是初始化数据的时间。

    我在我个人的电脑上多次测试,这个时间一直在40-50ms,然后我们通过Object.freeze()方法,将heavyData变为非响应式的再试下:

    //...
    data() {
      return {
        heavyData: Object.freeze(heavyData)
      }
    }
    //...
    复制代码
    

    改完之后再试下,初始化数据的时间变成了0-1ms,快了有40ms,这40ms都是递归遍历heavyData执行Object.defineProperty的时间。

    那么,为什么Object.freeze()会有这样的效果呢?对某一对象使用Object.freeze()后,将不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。

    而 Vue 在将数据改造成响应式之前有个判断:

    export function observe(value, asRootData) {
      // ...省略其他逻辑
      if (
        shouldObserve &&
        !isServerRendering() &&
        (Array.isArray(value) || isPlainObject(value)) &&
        Object.isExtensible(value) &&
        !value._isVue
      ) {
        ob = new Observer(value)
      }
      // ...省略其他逻辑
    }
    复制代码
    

    这个判断条件中有一个Object.isExtensible(value),这个方法是判断一个对象是否是可扩展的,由于我们使用了Object.freeze(),这里肯定就返回了false,所以就跳过了下面的过程,自然就省了很多时间。

    实际上,不止初始化数据时有影响,你可以用上面的例子统计下从createdmounted所用的时间,在我的电脑上不使用Object.freeze()时,这个时间是60-70ms,使用Object.freeze()后降到了40-50ms,这是因为在渲染函数中读取heavyData中的数据时,会执行到通过Object.defineProperty定义的getter方法,Vue 在这里做了一些收集依赖的处理,肯定就会占用一些时间,由于使用了Object.freeze()后的数据是非响应式的,没有了收集依赖的过程,自然也就节省了性能。

    由于访问响应式数据会走到自定义 getter 中并收集依赖,所以平时使用时要避免频繁访问响应式数据,比如在遍历之前先将这个数据存在局部变量中,尤其是在计算属性、渲染函数中使用,关于这一点更具体的说明,你可以看黄奕老师的这篇文章:Local variables

    但是这样做也不是没有任何问题的,这样会导致heavyData下的数据都不是响应式数据,你对这些数据使用computedwatch等都不会产生效果,不过通常来说这种大量的数据都是展示用的,如果你有特殊的需求,你可以只对这种数据的某一层使用Object.freeze(),同时配合使用上文中的延迟渲染、函数式组件等,可以极大提升性能。

    十、模板编译和渲染函数、JSX 的性能差异

    Vue 项目不仅可以使用 SFC 的方式开发,也可以使用渲染函数或 JSX 开发,很多人认为仅仅是只是开发方式不同,却不知这些开发方式之间也有性能差异,甚至差异很大,这一节我就找些例子来说明下,希望你以后在选择开发方式时有更多衡量的标准。

    其实 Vue2 模板编译中的性能优化不多,Vue3 中有很多,Vue3 通过编译和运行时结合的方式提升了很大的性能,但是由于本篇文章讲的是 Vue2 的性能优化,并且 Vue2 现在还是有很多人在使用,所以我就挑 Vue2 模板编译中的一点来说下。

    静态节点

    下面这个模板:

    <div>你好! <span>Hello</span></div>
    复制代码
    

    会被编译成:

    function render() {
      with (this) {
        return _m(0)
      }
    }
    复制代码
    

    可以看到和普通的渲染函数是有些不一样的,下面我们来看下为什么会编译成这样的代码。

    Vue 的编译会经过optimize过程,这个过程中会标记静态节点,具体内容可以看黄奕老师写的这个文档:Vue2 编译 - optimize 标记静态节点。

    codegen阶段判断到静态节点的标记会走到genStatic的分支:

    function genStatic(el, state) {
      el.staticProcessed = true
      const originalPreState = state.pre
      if (el.pre) {
        state.pre = el.pre
      }
      state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
      state.pre = originalPreState
      return `_m(${state.staticRenderFns.length - 1}${
        el.staticInFor ? ',true' : ''
      })`
    }
    复制代码
    

    这里就是生成代码的关键逻辑,这里会把渲染函数保存在staticRenderFns里,然后拿到当前值的下标生成_m函数,这就是为什么我们会得到_m(0)

    这个_m其实是renderStatic的缩写:

    export function renderStatic(index, isInFor) {
      const cached = this._staticTrees || (this._staticTrees = [])
      let tree = cached[index]
      if (tree && !isInFor) {
        return tree
      }
      tree = cached[index] = this.$options.staticRenderFns[index].call(
        this._renderProxy,
        null,
        this
      )
      markStatic(tree, `__static__${index}`, false)
      return tree
    }
    
    function markStatic(tree, key) {
      if (Array.isArray(tree)) {
        for (let i = 0; i < tree.length; i++) {
          if (tree[i] && typeof tree[i] !== 'string') {
            markStaticNode(tree[i], `${key}_${i}`, isOnce)
          }
        }
      } else {
        markStaticNode(tree, key, isOnce)
      }
    }
    
    function markStaticNode(node, key, isOnce) {
      node.isStatic = true
      node.key = key
      node.isOnce = isOnce
    }
    复制代码
    

    renderStatic的内部实现比较简单,先是获取到组件实例的_staticTrees,如果没有就创建一个,然后尝试从_staticTrees上获取之前缓存的节点,获取到的话就直接返回,否则就从staticRenderFns上获取到对应的渲染函数执行并将结果缓存到_staticTrees上,这样下次再进入这个函数时就会直接从缓存上返回结果。

    拿到节点后还会通过markStatic将节点打上isStatic等标记,标记为isStatic的节点会直接跳过patchVnode阶段,因为静态节点是不会变的,所以也没必要 patch,跳过 patch 可以节省性能。

    通过编译和运行时结合的方式,可以帮助我们很好的提升应用性能,这是渲染函数 / JSX 很难达到的,当然不是说不能用 JSX,相比于模板,JSX 更加灵活,两者有各自的使用场景。在这里写这些是希望能给你提供一些技术选型的标准。

    Vue2 的编译优化除了静态节点,还有插槽,createElement 等。

    Vue3 的模板编译优化

    相比于 Vue2,Vue3 中的模板编译优化更加突出,性能提升的更多,由于涉及的比较多,本篇文章写不下,如果你感兴趣的话你可以看看这些文章:Vue3 Compiler 优化细节,如何手写高性能渲染函数,聊聊 Vue.js 3.0 的模板编译优化,以及尤雨溪的解读视频:Vue 之父尤雨溪深度解读 Vue3.0 的开发思路,以后我也会单独写一些文章分析 Vue3 的模板编译优化。

    展开全文
  • 作者:我是你的超级英雄https://zhuanlan.zhihu.com/p/83180326前言Vue 框架通过数据双向绑定和虚拟 DOM 技术,帮我们处理了前端开发中最脏最累的 DO...

    作者:我是你的超级英雄

    https://zhuanlan.zhihu.com/p/83180326

    前言

    Vue 框架通过数据双向绑定和虚拟 DOM 技术,帮我们处理了前端开发中最脏最累的 DOM 操作部分, 我们不再需要去考虑如何操作 DOM 以及如何最高效地操作 DOM;但 Vue 项目中仍然存在项目首屏优化、Webpack 编译配置优化等问题,所以我们仍然需要去关注 Vue 项目性能方面的优化,使项目具有更高效的性能、更好的用户体验。本文是作者通过实际项目的优化实践进行总结而来,希望读者读完本文,有一定的启发思考,从而对自己的项目进行优化起到帮助。本文内容分为以下三部分组成:

    • Vue 代码层面的优化;

    • webpack 配置层面的优化;

    • 基础的 Web 技术层面的优化。

    一、代码层面的优化

    1.1、v-if 和 v-show 区分使用场景

    v-if 是 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

    v-show 就简单得多, 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 display 属性进行切换。

    所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。

    1.2、computed 和 watch 区分使用场景

    computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;

    watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;

    运用场景:

    • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;

    • 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

    1.3、v-for 遍历必须为 item 添加 key,且避免同时使用 v-if

    (1)v-for 遍历必须为 item 添加 key

    在列表数据进行遍历渲染时,需要为每一项 item 设置唯一 key 值,方便 Vue.js 内部机制精准找到该条列表数据。当 state 更新时,新的状态值和旧的状态值对比,较快地定位到 diff 。

    (2)v-for 遍历避免同时使用 v-if

    v-for 比 v-if 优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成 computed 属性。

    推荐:

    <ul>
      <li
        v-for="user in activeUsers"
        :key="user.id">
        {{ user.name }}
      </li>
    </ul>
    computed: {
      activeUsers: function () {
        return this.users.filter(function (user) {
     return user.isActive
        })
      }
    }
    复制代码
    

    不推荐:

    <ul>
      <li
        v-for="user in users"
        v-if="user.isActive"
        :key="user.id">
        {{ user.name }}
      </li>
    </ul>
    复制代码
    

    1.4、长列表性能优化

    Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止 Vue 劫持我们的数据呢?可以通过 Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。

    export default {
      data: () => ({
        users: {}
      }),
      async created() {
        const users = await axios.get("/api/users");
        this.users = Object.freeze(users);
      }
    };
    复制代码
    

    1.5、事件的销毁

    Vue 组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。如果在 js 内使用 addEventListene 等方式是不会自动销毁的,我们需要在组件销毁时手动移除这些事件的监听,以免造成内存泄露,如:

    created() {
      addEventListener('click', this.click, false)
    },
    beforeDestroy() {
      removeEventListener('click', this.click, false)
    }
    复制代码
    

    1.6、图片资源懒加载

    对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue 的 vue-lazyload 插件:

    (1)安装插件

    npm install vue-lazyload --save-dev
    复制代码
    

    (2)在入口文件 man.js 中引入并使用

    import VueLazyload from 'vue-lazyload'
    复制代码
    

    然后再 vue 中直接使用

    Vue.use(VueLazyload)
    复制代码
    

    或者添加自定义选项

    Vue.use(VueLazyload, {
    preLoad: 1.3,
    error: 'dist/error.png',
    loading: 'dist/loading.gif',
    attempt: 1
    })
    复制代码
    

    (3)在 vue 文件中将 img 标签的 src 属性直接改为 v-lazy ,从而将图片显示方式更改为懒加载显示:

    <img v-lazy="/static/img/1.png">
    复制代码
    

    以上为 vue-lazyload 插件的简单使用,如果要看插件的更多参数选项,可以查看 vue-lazyload 的 github 地址。

    1.7、路由懒加载

    Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak 打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来。

    路由懒加载:

    const Foo = () => import('./Foo.vue')
    const router = new VueRouter({
      routes: [
        { path: '/foo', component: Foo }
      ]
    })
    复制代码
    

    1.8、第三方插件的按需引入

    我们在项目中经常会需要引入第三方插件,如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助 babel-plugin-component ,然后可以只引入需要的组件,以达到减小项目体积的目的。以下为项目中引入 element-ui 组件库为例:

    (1)首先,安装 babel-plugin-component :

    npm install babel-plugin-component -D
    复制代码
    

    (2)然后,将 .babelrc 修改为:

    {
      "presets": [["es2015", { "modules": false }]],
      "plugins": [
        [
          "component",
          {
            "libraryName": "element-ui",
            "styleLibraryName": "theme-chalk"
          }
        ]
      ]
    }
    复制代码
    

    (3)在 main.js 中引入部分组件:

    import Vue from 'vue';
    import { Button, Select } from 'element-ui';
    
     Vue.use(Button)
     Vue.use(Select)
    复制代码
    

    1.9、优化无限列表性能

    如果你的应用存在非常长或者无限滚动的列表,那么需要采用 窗口化 的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom 节点的时间。你可以参考以下开源项目 vue-virtual-scroll-list 和 vue-virtual-scroller 来优化这种无限列表的场景的。

    1.10、服务端渲染 SSR or 预渲染

    服务端渲染是指 Vue 在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的 html 片段直接返回给客户端这个过程就叫做服务端渲染。

    (1)服务端渲染的优点:

    • 更好的 SEO:因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;

    • 更快的内容到达时间(首屏加载更快):SPA 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间;

    (2)服务端渲染的缺点:

    • 更多的开发条件限制:例如服务端渲染只支持 beforCreate 和 created 两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于 Node.js server 运行环境;

    • 更多的服务器负载:在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用CPU 资源,因此如果你预料在高流量环境下使用,请准备相应的服务器负载,并明智地采用缓存策略。

    如果你的项目的 SEO 和 首屏渲染是评价项目的关键指标,那么你的项目就需要服务端渲染来帮助你实现最佳的初始加载性能和 SEO,具体的 Vue SSR 如何实现,可以参考作者的另一篇文章《Vue SSR 踩坑之旅》。如果你的 Vue 项目只需改善少数营销页面(例如  /, /about, /contact 等)的 SEO,那么你可能需要预渲染,在构建时 (build time) 简单地生成针对特定路由的静态 HTML 文件。优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点,具体你可以使用 prerender-spa-plugin 就可以轻松地添加预渲染 。

    二、Webpack 层面的优化

    2.1、Webpack 对图片进行压缩

    在 vue 项目中除了可以在 webpack.base.conf.js 中 url-loader 中设置 limit 大小来对图片处理,对小于 limit 的图片转化为 base64 格式,其余的不做操作。所以对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用 image-webpack-loader来压缩图片:

    (1)首先,安装 image-webpack-loader :

    npm install image-webpack-loader --save-dev
    复制代码
    

    (2)然后,在 webpack.base.conf.js 中进行配置:

    {
      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
      use:[
        {
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('img/[name].[hash:7].[ext]')
          }
        },
        {
          loader: 'image-webpack-loader',
          options: {
            bypassOnDebug: true,
          }
        }
      ]
    }
    复制代码
    

    2.2、减少 ES6 转为 ES5 的冗余代码

    Babel 插件会在将 ES6 代码转换成 ES5 代码时会注入一些辅助函数,例如下面的 ES6 代码:

    class HelloWebpack extends Component{...}
    复制代码
    

    这段代码再被转换成能正常运行的 ES5 代码时需要以下两个辅助函数:

    babel-runtime/helpers/createClass  // 用于实现 class 语法
    babel-runtime/helpers/inherits  // 用于实现 extends 语法
    复制代码
    

    在默认情况下, Babel 会在每个输出文件中内嵌这些依赖的辅助函数代码,如果多个源代码文件都依赖这些辅助函数,那么这些辅助函数的代码将会出现很多次,造成代码冗余。为了不让这些辅助函数的代码重复出现,可以在依赖它们时通过 require('babel-runtime/helpers/createClass') 的方式导入,这样就能做到只让它们出现一次。babel-plugin-transform-runtime 插件就是用来实现这个作用的,将相关辅助函数进行替换成导入语句,从而减小 babel 编译出来的代码的文件大小。

    (1)首先,安装 babel-plugin-transform-runtime :

    npm install babel-plugin-transform-runtime --save-dev
    复制代码
    

    (2)然后,修改 .babelrc 配置文件为:

    "plugins": [
        "transform-runtime"
    ]
    复制代码
    

    如果要看插件的更多详细内容,可以查看babel-plugin-transform-runtime 的 详细介绍。

    2.3、提取公共代码

    如果项目中没有去将每个页面的第三方库和公共模块提取出来,则项目会存在以下问题:

    • 相同的资源被重复加载,浪费用户的流量和服务器的成本。

    • 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。

    所以我们需要将多个页面的公共代码抽离成单独的文件,来优化以上问题 。Webpack 内置了专门用于提取多个Chunk 中的公共部分的插件 CommonsChunkPlugin,我们在项目中 CommonsChunkPlugin 的配置如下:

    // 所有在 package.json 里面依赖的包,都会被打包进 vendor.js 这个文件中。
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: function(module, count) {
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        );
      }
    }),
    // 抽取出代码模块的映射关系
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      chunks: ['vendor']
    })
    复制代码
    

    如果要看插件的更多详细内容,可以查看 CommonsChunkPlugin 的 详细介绍。

    2.4、模板预编译

    当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。

    预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。

    如果你使用 webpack,并且喜欢分离 JavaScript 和模板文件,你可以使用 vue-template-loader,它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数。

    2.5、提取组件的 CSS

    当使用单文件组件时,组件内的 CSS 会以 style 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段 “无样式内容闪烁 (fouc) ” 。将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存。

    查阅这个构建工具各自的文档来了解更多:

    • webpack + vue-loader ( vue-cli 的 webpack 模板已经预先配置好)

    • Browserify + vueify

    • Rollup + rollup-plugin-vue

    2.6、优化 SourceMap

    我们在项目进行打包后,会将开发中的多个文件代码打包到一个文件中,并且经过压缩、去掉多余的空格、babel编译化后,最终将编译得到的代码会用于线上环境,那么这样处理后的代码和源代码会有很大的差别,当有 bug的时候,我们只能定位到压缩处理后的代码位置,无法定位到开发环境中的代码,对于开发来说不好调式定位问题,因此 sourceMap 出现了,它就是为了解决不好调式代码问题的。

    SourceMap 的可选值如下(+ 号越多,代表速度越快,- 号越多,代表速度越慢, o 代表中等速度 )

    1.png

    开发环境推荐:cheap-module-eval-source-map

    生产环境推荐:cheap-module-source-map

    原因如下:

    • cheap:源代码中的列信息是没有任何作用,因此我们打包后的文件不希望包含列相关信息,只有行信息能建立打包前后的依赖关系。因此不管是开发环境或生产环境,我们都希望添加 cheap 的基本类型来忽略打包前后的列信息;

    • module :不管是开发环境还是正式环境,我们都希望能定位到bug的源代码具体的位置,比如说某个 Vue 文件报错了,我们希望能定位到具体的 Vue 文件,因此我们也需要 module 配置;

    • soure-map :source-map 会为每一个打包后的模块生成独立的 soucemap 文件 ,因此我们需要增加source-map 属性;

    • eval-source-map:eval 打包代码的速度非常快,因为它不生成 map 文件,但是可以对 eval 组合使用 eval-source-map 使用会将 map 文件以 DataURL 的形式存在打包后的 js 文件中。在正式环境中不要使用 eval-source-map, 因为它会增加文件的大小,但是在开发环境中,可以试用下,因为他们打包的速度很快。

    2.7、构建结果输出分析

    Webpack 输出的代码可读性非常差而且文件非常大,让我们非常头疼。为了更简单、直观地分析输出结果,社区中出现了许多可视化分析工具。这些工具以图形的方式将结果更直观地展示出来,让我们快速了解问题所在。接下来讲解我们在 Vue 项目中用到的分析工具:webpack-bundle-analyzer 。

    我们在项目中 webpack.prod.conf.js 进行配置:

    if (config.build.bundleAnalyzerReport) {
      var BundleAnalyzerPlugin =   require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
      webpackConfig.plugins.push(new BundleAnalyzerPlugin());
    }
    复制代码
    

    执行 $ npm run build \--report 后生成分析报告如下:

    1.png

    2.8、Vue 项目的编译优化

    如果你的 Vue 项目使用 Webpack 编译,需要你喝一杯咖啡的时间,那么也许你需要对项目的 Webpack 配置进行优化,提高 Webpack 的构建效率。具体如何进行 Vue 项目的 Webpack 构建优化,可以参考作者的另一篇文章《 Vue 项目 Webpack 优化实践》

    三、基础的 Web 技术优化

    3.1、开启 gzip 压缩

    gzip 是 GNUzip 的缩写,最早用于 UNIX 系统的文件压缩。HTTP 协议上的 gzip 编码是一种用来改进 web 应用程序性能的技术,web 服务器和客户端(浏览器)必须共同支持 gzip。目前主流的浏览器,Chrome,firefox,IE等都支持该协议。常见的服务器如 Apache,Nginx,IIS 同样支持,gzip 压缩效率非常高,通常可以达到 70% 的压缩率,也就是说,如果你的网页有 30K,压缩之后就变成了 9K 左右

    以下我们以服务端使用我们熟悉的 express 为例,开启 gzip 非常简单,相关步骤如下:

    • 安装:

    npm install compression --save
    复制代码
    
    • 添加代码逻辑:

    var compression = require('compression');
    var app = express();
    app.use(compression())
    复制代码
    
    • 重启服务,观察网络面板里面的 response header,如果看到如下红圈里的字段则表明 gzip 开启成功 :

      1.png

    3.2、浏览器缓存

    为了提高用户加载页面的速度,对静态资源进行缓存是非常必要的,根据是否需要重新向服务器发起请求来分类,将 HTTP 缓存规则分为两大类(强制缓存,对比缓存),如果对缓存机制还不是了解很清楚的,可以参考作者写的关于 HTTP 缓存的文章《深入理解HTTP缓存机制及原理》,这里不再赘述。

    3.3、CDN 的使用

    浏览器从服务器上下载 CSS、js 和图片等文件时都要和服务器连接,而大部分服务器的带宽有限,如果超过限制,网页就半天反应不过来。而 CDN 可以通过不同的域名来加载文件,从而使下载文件的并发连接数大大增加,且CDN 具有更好的可用性,更低的网络延迟和丢包率 。

    3.4、使用 Chrome Performance 查找性能瓶颈

    Chrome 的 Performance 面板可以录制一段时间内的 js 执行细节及时间。使用 Chrome 开发者工具分析页面性能的步骤如下。

    1. 打开 Chrome 开发者工具,切换到 Performance 面板

    2. 点击 Record 开始录制

    3. 刷新页面或展开某个节点

    4. 点击 Stop 停止录制

    1.png

    更多关于 Performance 的内容可以点击这里查看。

    总结

    本文通过以下三部分组成:Vue 代码层面的优化、webpack 配置层面的优化、基础的 Web 技术层面的优化;来介绍怎么去优化 Vue 项目的性能。希望对读完本文的你有帮助、有启发,如果有不足之处,欢迎批评指正交流!

    最后

    1. 感谢阅读,欢迎分享给身边的朋友,

    2. 记得关注噢,黑叔带你飞!

    亲,点这涨工资 

    展开全文
  • 你有给自己的项目过性能优化吗? 据统计53%的用户不会等待超过3秒就会关闭掉网页,所以对于项目的性能优化尤为的重要。 闲话少说,今天我们来详细了解如何优化Vue项目的性能。 分析项目bundle大小 打包后的文件...

    你知道你的项目的首次加载时间需要多久吗?你有给自己的项目做过性能优化吗?

    据统计53%的用户不会等待超过3秒就会关闭掉网页,所以对于项目的性能优化尤为的重要。

    闲话少说,今天我们来详细了解如何优化Vue项目的性能。

    分析项目bundle大小

    打包后的文件大小直接影响我们访问的加载速度,所以我们要知道哪些打包文件存在性能问题。

    查看bundle大小主要有两种方法

    report

    使用webpack-bundle-analyzer来生成一个报告,可以清晰直观的看出每个打包后文件的大小

    我们在Vue项目的package.json文件中加入下面的命令

    "build-report": "vue-cli-service build --report"
    

    然后,在命令行中运行

    npm run build-report
    

    等待一段时间后,会在dist文件中生成一个report.html文件,我们在浏览器中打开它

    image-20211014204030329

    build

    在本地使用build打包命令

    npm run build
    

    然后看终端的输出信息

    image-20211014214919867

    可以看到主要的文件从大到小的排序

    在查看完关键的信息后,我们可以做一些优化手段

    1. 懒加载模块

    比如Echarts图标的包和编辑器的包,都是很大的有45百kb,但并不是所有的页面都需要图表和编辑器,只有少量页面需要,所以我们可以把Echarts和编辑器放到需要的页面中去加载,不要放到全局里面,这样其他页面就省去加载他们的时间。

    要查看浏览器加载文件的顺序,我们打开浏览器的开发者工具,点击Network,勾选上Disable cache,这样不会从浏览器的缓存中去加载文件,然后重新去刷新页面,可以看到所有文件的加载顺序

    image-20211014205215500

    一般我引入模块是这样写

    // demo.js
    const Demo = {
      testDemo: function () {
        console.log("这是DEMO")
      }
    }
    export default Demo
    
    // app.js
    import Demo from './demo.js'
    Demo.testDemo()
    

    使用懒加载这样写

    // app.js
    const getDemo = () => import('./demo.js')
    
    // 延迟加载
    getDemo()
      .then({ testDemo } => testDemo())
    

    懒加载能够非常好的解决bundle过大的问题,把一些用户并不常用的功能放到懒加载里,当用户点击或者滚动可视区再进行加载。

    2. 路由懒加载拆分

    一般我们的路由可能这样写,这样写在打包时把Dashboard组件和Contact组件的代码全部打包到JS中。

    // routing.js
    import Dashboard from './Dashboard.vue'
    import Contact from './Contact.vue'
    
    const routes = [
      { path: '/', component: Dashboard }
      { path: '/contact', component: Contact }
    ]
    

    我们可以按路由去懒加载,改成下面这样

    // routing.js 
    const routes = [
      { path: '/', component: () => import('./Dashboard.vue') }
      { path: '/contact', component: () => import('./Contact.vue') }
    ]
    

    这样的好处就是,我们没有访问路由时/contact,就不会去加载Contact.vue的代码,只有访问到时才会去加载组件的代码。

    通过webpackChunkName来聚合打包多个页面到一个bundle中

    一般我们的一个页面的bundle可能非常的小,我们可以把一个模块的所有路由的页面都打包到一个bundle中,在import函数加入/* webpackChunkName:'group-superAdmin' */,其中webpackChunkName的名称需要自己定义。

    const router = [
      {
        path: 'superAdminAccountList',
        name: 'SuperAdminAccountList',
        component: () => import(/* webpackChunkName:'group-superAdmin' */ '@/activity/superAdmin/AccountList'),
      },
      {
        path: 'superAdminCreateAccount',
        name: 'SuperAdminCreateAccount',
        component: () => import(/* webpackChunkName:'group-superAdmin' */ '@/activity/superAdmin/CreateAccount'),
      },
      {
        path: 'superAdminRoleList',
        name: 'SuperAdminRoleList',
        component: () => import(/* webpackChunkName:'group-superAdmin' */ '@/activity/superAdmin/RoleList'),
      },
      {
        path: 'superAdminCreateRole/:id?/:look?',
        name: 'SuperAdminCreateRole',
        component: () => import(/* webpackChunkName:'group-superAdmin' */ '@/activity/superAdmin/CreateRole'),
      },
    ];
    

    3. prefetch预渲染组件

    prefetch是一种利用浏览器的空闲时间加载页面将来可能用到的资源的一种机制;通常可以用于加载非首页的其他页面所需要的资源,以便加快后续页面的首屏速度;

    通过webpack我们可以非常方便的实现组件的预渲染,通过加入/* webpackPrefetch: true */,webpack会自动在页面中加入** 的标签。

    components: {
      ModalView: () => import(/* webpackPrefetch: true */ './ModalView.vue')
    }
    

    4. 优化三方库依赖

    一般我们在项目中都会用到一些三方的依赖库,比如lodash

    在这里插入图片描述

    如果我们全量的引入的话会很大,但我们可能只用到一两个函数,我们只需要引用我们需要的就好

    import isEmpty from 'lodash/isEmpty`
    

    5. 善用浏览器缓存

    我们都知道我们访问一个网站后,浏览器会对JS和CSS去进行缓存,在下次访问的时候会从缓存中去获取文件,不用再去请求。你可能会问浏览器缓存我们也控制不了?我们怎么做呢?

    浏览器缓存

    一般我们打包后的文件像这样

    • main.[hash].js – 根组件
    • common.[hash].js – 公共组件
    • dashboard.[hash].js – dashboard页面
    • contact.[hash].js – contact页面

    其实我们可以把一些很长时间都不需要的公共代码依赖放到common.[hash].js中,这样一次缓存后,以后再次访问就不会去发请求。

    6. 优化压缩图片

    图片的大小对于项目的性能也至关重要,一般webpack会帮助我们把小的图片直接转为base64来减少网络请求。对其他图片来说我们也要进行压缩,一般压缩方式有两种

    1. 使用软件进行压缩
    2. 使用CDN进行文件压缩

    使用软件压缩我推荐使用TinyPNG,一个在线网站,使用它压缩几乎不损失清晰度而且压缩效果特别好。

    使用CDN压缩,一般专业的文件存储都会提供图片的处理功能比如京东的图片地址,中间的s280x280可以去修改图片宽高,后面的.webp后缀把jpg图片转换成webp格式,进一步的来缩小文件的大小。

    https://img30.360buyimg.com/seckillcms/s280x280_jfs/t1/187871/40/5785/69529/60b4af7dE5a50eaff/c235bb87f19c1698.jpg.webp
    

    7. 静态文件上CDN

    一般小公司可能为了方便直接把CSS、JS、图片等文件直接传到服务器上进行访问,使用CDN的优势在于CDN时全国的各个地方都会有服务节点,而且CDN也会缓存文件,所以通过CDN访问静态文件和直接访问服务器文件要快上几倍。

    淘宝的图片访问,有98%的流量都走了CDN缓存。只有2%会回源到源站,节省了大量的服务器资源。

    总结

    通过上面的一波操作,我们的项目首屏加载和淘宝京东网站几乎差不多了,还有其他更多优化办法,欢迎您再评论区告诉我,我们相互学习。

    如果本文有帮助,微信搜索【小帅的编程笔记】,每天一个小知识

    展开全文
  • Vue2性能优化

    2021-09-28 11:26:49
    子组件拆分 优化前: {{ heavy() }} 优化后: 优化后的方式是把这个耗时任务 heavy 函数的执行逻辑用子组件 ChildComp 封装了,由于 Vue 的更新是组件粒度的,虽然每一帧都通过数据修改导致了父组件的重新渲染,但是...
  • vue项目优化 Vue框架通过数据绑定和DOM虚拟技术, 处理了前端DOM操作, 在vue中不在去考虑如何操作DOM, vue中仍然存在项目首屏优化,webpack打包优化优化部分从下面几部分: Vue代码层面的优化 webpack配置层面的...
  • vue性能优化方案总结

    2021-03-29 23:02:45
    vue性能优化方案总结 1.问题描述: 目前Vue虽然被广泛应用,但是其存在项目首屏优化,Webpack编译配置优化等问题,所以我们仍然需要去关注Vue项目性能方面的优化,使项目具有更高效的性能,更好的用户体验。 2....
  • vue性能优化

    2021-07-25 19:25:44
    vue性能优化前言函数式组件冻结列表数据子组件拆分局部变量(缓存变量)computed 的缓存特性v-if 和 v-for 不要同时出现不需要渲染在视图的数据不要写在 data 中v-for 中的 keykeep-alive渐进式渲染时间片切割组件懒...
  • vue2 性能优化2

    2021-02-14 12:00:25
    如果在同一个组件里面了很多复杂操作操作,进行数据修改时候会进行 依赖收集,但是一个组件里面只存在一个watcher。触发这一个watcher会把data里面的所有数据重新进行一次收集造成性能浪费 但是你把复杂逻辑拆分...
  • 我们在使用 Vue 或其他框架的日常开发中,或多或少的都会遇到一些性能问题,尽管 Vue 内部已经帮助我们了许多优化,但是还是有些问题是需要我们主动去避免的。我在我的日常开中,以及网上各种大佬的文章中总结了...
  • [vue] 使用vue后怎么针对搜索引擎SEO优化? 1.SSR服务器渲染; 2.静态化; 3.预渲染prerender-spa-plugin; 4.使用Phantomjs针对爬虫处理 个人简介 我是歌谣,欢迎和大家一起交流前后端知识。放弃很容易, 但...
  • vue项目优化方案(性能,包体积,页面加载速度等) 项目优化,是一个老生常谈的问题。这里简单总结一下相关方法: 1.代码优化,代码模块化,组件分离复用,工具函数抽离,注意代码精简,v-for渲染加上key属性,for...
  • vue项目优化

    2021-10-27 16:17:18
    一、代码优化 :1. 使用keep-alive缓存不活动的组件 二. 使用路由懒加载: 三:图片懒加载 四、 使用节流防抖函数(性能优化) 五、v-for 遍历必须为 item 添加 key,且避免同时使用 v-if 六、v-if 和 v-show ...
  • 比如本章要讲的性能优化纯粹是因为之前的代码过于暴力求解,需要一些“设计模式”来重新优化一下之前写的不怎么漂亮的代码。 除此之外,之前遗留的一些历史问题也因为编译工具提供的一些新的API(Proxy)成功解决了...
  • VUE 渲染性能优化

    千次阅读 2020-12-25 01:19:46
    VUE 渲染性能优化 项目地址 教你如何渲染 5000 个 svg图标不卡顿 这是无意间发现的一个课题,引起了我的兴趣… 事情是这样的,有一天我发现,项目在进入路由icon 集合页面时,页面有概率出现短暂的顿挫感 像这样 看...
  • vue 项目如何快速优化

    2021-03-18 17:23:15
    Vue在前端框架中的地位就像曾经的 jQuery,由于其简单易懂、开发效率高,已经成为了前端工程师必不可少的技能之一。无论是BAT 等大厂,还是小型初创公司,Vue 都有着广泛的应用,其...
  • VUE 性格优化

    2021-03-05 13:47:37
    一、vue-router路由懒加载 懒加载是当路由被访问的时候才加载对应组件,而不是在首页就全部加载,以此来提高首页反应速度。 方法和原理 require-ensure  说明: require.ensure在需要的时候才下载依赖的模块,当...
  • 目录前言一、 Vue 项目进行 SEO 优化1. SSR服务器渲染2. 静态化 (博客, 介绍性官网)3. 预渲染 prerender-spa-plugin (插件)4. 使用Phantomjs `针对爬虫` 处理小结总结 前言 众所周知,vue项目是单页面的应用,是...
  • Vue项目打包编译优化

    2020-12-24 10:34:53
    Vue项目打包编译优化当一个较复杂的vue项目打包后,文件会非常大,而且访问时资源加载速度很慢,所以采取了几种措施来优化。1. 不生成.map文件默认情况下,当我们执行 npm run build 命令打包完一个项目后,会得到一...
  • vue列表页渲染优化,具体内容如下想法初始化时,vue会对datagetter、setter改造,在现代浏览器里,虽然JS已经足够快,但仍然有优化空间。列表页的数据结构为:Vue会给数组中的每个值设置getter和setter来监听它们...
  • Vue如何优化首页加载

    2021-12-14 15:11:20
    首页白屏原因 主要原因是单页应用,加载资源过慢,需要将所有需要的资源都下载到浏览器...vue-server-renderer yue-skeleton-webpack-plugin page-skeleton-webpack-plugin 2,使用首屏SSR +跳转SPA方式来优化 3,改单页应
  • Vue性能优化技巧总结

    2021-02-22 20:39:27
    Vue性能优化技巧 第一个技巧,函数式组件 可以降低 优化执行 script 的时间 减少线程阻塞的情况。 缺点 不维护响应数据 无钩子函数 没有instance实例 所以在组件内部没有办法像传统组件一样通过this来访问组件属性 ...
  • 创建vue.config.js文件:2.开启CDN加速:3.压缩代码:4.公共代码抽离,写在configureWebpack模块中5.骨架屏 1.创建vue.config.js文件: vue-cli3 脚手架搭建完成后,项目目录中没有 vue.config.js 文件,需要手动...
  • vue 性能优化

    2021-12-17 16:50:48
    参考项目:vue-virtual-scroller、vue-virtual-scroll-list 图片按需加载 // 对于图片过多的页面,为了加速页面的加载速度,所以需要我们将页面内未出现在可视区域内的图片先不加载,等到滚动到可视区域后再去加载...
  • Vue项目性能优化是个老生常谈的问题了,本人开发过程中也查过很多关于Vue项目优化的文章,每篇文章说的都差不多,本章我就结合我的心得和大家的智慧一个总结。1.懒加载懒加载应该是提高性能的最简单有效的方式了,...
  • 导语|Vue是一套用于构建用户界面的渐进式的JavaScript框架。它具有体积小,更高的运行效率,双向数据绑定,生态丰富、学习成本低等优点,所以Vue也被广泛用在移动端跨平台框架上。...
  • vue 优化体验

    2020-12-24 13:26:36
    一、全局样式 不通过import 在项目中任何使用变量和mixins // vue.config.js ...使用 nprogress 对路由跳转进行一个伪进度条 在网络不好的情况下然用户体验优化 import NProgress from 'nprogress'
  • Vue的seo优化

    2021-09-18 14:20:50
    Vue的seo优化vue.config.jsmain.jsa.vue异常处理 基于vue2、vue-meta-info(0.1.7)和prerender-spa-plugin(3.4.0) vue.config.js const PrerenderSPAPlugin = require('prerender-spa-plugin'); const Renderer = ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 41,985
精华内容 16,794
关键字:

vue怎么做优化

vue 订阅