1. Vue 项目优化方式有哪些?

  • 使用 计算属性特点:可以被缓存

  • 使用 函数式组件

    例如:对于某些组件,如果我们只是用来显示一些数据,不需要管理状态,监听数据等,那么就可以用函数式组件。

    函数式组件无状态的无实例的,在初始化时不需要初始化状态,不需要创建实例,也不需要去处理生命周期等,相比有状态组件,会更加轻量,同时性能也更好。

  • 结合场景使用 v-showv-if

    两者的作用: 都是用来控制某些组件或 DOM 的显示/隐藏

    v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景

    v-show 适用于需要非常频繁切换条件的场景

    1. v-if 的部分被转换成了一个三元表达式, visibletrue 时,创建组件的 vnode,否则创建一个空 vnode。在 patch 的时候,新旧节点不一样,就会移除旧的节点或创建新的节点,这样的话组件也会跟着创建/销毁。如果组件里有很多 DOM,或者要执行很多初始化/销毁逻辑,那么随着 visible 的切换,势必会浪费掉很多性能
    2. v-show 被编译成了 directives,它其实是通过切换元素的 display 属性 来控制的,和 v-if 相比,不需要在 patch 阶段 创建/移除节点,只是根据 v-show 上绑定的值来控制 DOM 元素style.display 属性,在频繁切换的场景下就可以节省很多性能

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

    总结v-if 的优势体现在 初始化 时,v-show 体现在 更新 时(初始化性能压力大)

  • 使用 keep-alive

    keep-alive 的作用 就是将它包裹的组件在第一次渲染后就缓存起来,下次需要时就直接从缓存里面取,避免了不必要的性能浪费

  • 避免 v-forv-if 同时使用

    Vue2 中v-for 的优先级比 v-if 高;Vue3 中v-if 的优先级比 v-for

    总结:使用计算属性代替

  • v-for 添加 key, 并且不要将 index 或者 随机数 作为 key

  • 延迟渲染

    延迟渲染 就是 分批渲染,假设我们某个页面里有一些组件在初始化时需要执行复杂的逻辑,这将会占用很长时间,导致帧数下降、卡顿,其实可以使用分批渲染的方式来进行优化,就是先渲染一部分,再渲染另一部分

    <template>
        <Heavy v-if="defer(1)" />
    </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, 也就会带来性能问题,这个时候我们可以强制使数据变为非响应式,从而节省时间

    解决方法Object.freeze()

    扩展:为什么 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,所以就跳过了下面数据劫持的过程,没有了 收集依赖的过程,自然也就节省了性能。

    问题:数据都不是响应式的,可以只对这种数据的某一层使用 Object.freeze(),同时配合使用上文中的 延迟渲染函数式组件 等,可以极大提升性能。

  • computedwatchmethods 区分使用场景

    1. computed: 一个数据受多个数据影响的。
    2. watch: 一个数据影响多个数据的。当数据变化时,需要执行异步或开销较大的操作时。如果数据变化时请求一个接口。
    3. methods: 希望数据是实时更新,不需要缓存。
  • 防抖和节流

    1. 防抖:触发事件后规定时间内事件只会执行一次。简单来说就是防止手抖,短时间操作了好多次。
    2. 节流:事件在规定时间内只执行一次。
    3. 应用场景: 节流不管事件有没有触发还是频繁触发,在规定时间内一定会只执行一次事件,而防抖是在规定时间内事件被触发,且是最后一次被触发才执行一次事件。
  • 图片大小优化和懒加载

    1. 图片大小的优化,可以用 image-webpack-loader 进行压缩图片
    2. 图片懒加载,可以用 vue-lazyload 插件实现 问题vue-lazyload 如何实现?考查 Vue 插件、指令
  • 利用挂载节点会被替换的特性优化白屏问题

    我们可以在<div id="app"></div>里添加首屏的静态页面。等真正的首屏加载出来后就会把<div id="app"></div>这块结构都替换掉,给人一种视觉上的误差,就不会产生白屏

  • 组件库按需引入 如 element UI 库,用 babel-plugin-component 插件实现按需引入

    在根目录下 .babelrc.js 文件中按如下配置:

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

    其中 libraryName 为组件库的名称,styleLibraryName 为组件库打包后样式存放的文件夹名称。

    问题babel-plugin-component 是如何做到按需加载的?

    其实 babel-plugin-component 插件是 element 用 babel-plugin-import 插件改造后特定给 element UI 使用。一般的组件库还是 babel-plugin-import 插件实现按需引入。

  • 打包优化:

    1. webpack-bundle-analyzer 可以帮助你可视化的分析打包后的各个资源的大小

    2. 利用 import() 异步引入组件实现按需引入

      路由懒加载,所谓的懒加载就是用 import() 异步引入组件

      component: () =>import('views/home.vue'),
      component: resolve =>require(['views/home.vue'],resolve)
      

      resolve 方式打包会把所有组件的代码都打包在一个 js 文件。预期应该是每个组件的代码都被打包成对应的 js 文件,加载组件时会对应加载 js 文件,这才是懒加载。

      • 可以使用 webpackChunkName: chunk 文件的名称, [request]表示实际解析的文件名
      function load(component) {
        return () =>
          import(/* webpackChunkName: "[request]" */ `views/${component}`)
      }
      
      • 使用懒加载后,浏览器的 network 就会出现 Purpose: prefetch,只是预取(prefetch)一下,没有返回内容的。目的是告诉浏览器,空闲的时候给我加载这个 js 文件。直到真正加载这个路由组件时,这个 js 文件再次被加载

      扩展preloadprefetch 的区别

      preload(预加载):可以强制浏览器在不阻塞 documentonload 事件的情况下请求资源

      prefetch(懒加载): 告诉浏览器这个资源将来可能需要,但是什么时间加载这个资源是由浏览器来决定的

    3. 利用 externals 提取第三方依赖并用 CDN 引入

      Webpack 中的 externals 配置选项,可避免将第三方依赖打包,而是在项目运行时从外部获取第三方依赖。

    4. 利用 SplitChunks 插件提取公共 js 代码分割 js 代码

    5. 压缩 图片HTMLCSSJS 等静态资源

  • 项目部署的优化

    1. 识别 gzip 压缩是否开启

      只要看响应头部(Response headers)中 有没有 Content-Encoding: gzip 这个属性即可,有代表有开启 gzip 压缩。

    2. 在 Nginx 上开启 gzip 压缩

      nginx/conf/nginx.conf 中配置

      http {
         // on | off ,默认为off,on为开启gzip,off为关闭gzip
         gzip  on;
         // number,压缩起点,文件大于多少才进行压缩,单位默认为字节,也可用k表示千字节
         gzip_min_length 1k;
         // 压缩级别,1-9,数字越大,压缩后的大小越小,也越占用CPU,花费时间越长
         gzip_comp_level 5;
         // 需要进行压缩的文件类型。类型去Response headers中看Content-Type属性
         gzip_types application/javascript image/png image/gif image/jpeg text/css text/plain;
         // number size,设置系统获取几个单位的缓存用于存储gzip的压缩结果数据流(例如 4 4k代表以4k为单位,按照原始数据大小以4k为单位的4倍申请内存。如原始数据大小为17K,则申请 (17/4)*4 = 17k内存)
         gzip_buffers 4 4k;
         // 设置gzip压缩针对的HTTP协议版本以上
         gzip_http_version 1.1;
         // on | off,是否在http header中添加Vary:Accept-Encoding,on表示添加。Vary:Accept-Encoding告诉代理服务器缓存两种版本的资源:压缩和非压缩,避免一个浏览器不支持压缩资源,而先请求了服务器,服务器缓存了非压缩的资源,然后一个浏览器支持压缩资源,再去请求了服务器,结果得到非压缩资源,但是又去解压它,结果会出错。所以建议设置为on
         gzip_vary on;
      }
      
    3. Webpack 上开启 gzip 压缩 利用 CompressionWebpack 插件来实现 gzip 压缩

      • CompressionWebpack 参数详解:

        1. test:String|RegExp|Array<String|RegExp>,资源的名称符合条件的才会被压缩,默认为 undefined,即全部符合,例如只要压缩 js 文件
        plugins: [
             new CompressionPlugin({
                 test: /\.js(\?.*)?$/i,
             })
         ],
        
        1. include:String|RegExp|Array<String|RegExp>,资源的名称符合条件的才会被压缩,默认为 undefined,是在 test 参数的范围内在进行筛选,满足 test 参数的条件,且满足 include 参数的条件的资源才会被压缩
        2. exclude:String|RegExp|Array<String|RegExp>,压缩时排除资源的名称符合条件的资源,默认为 undefined,是在 test 参数的范围内在进行排除,满足 test 参数的条件,不满足 exclude 参数的条件的资源才会被压缩
        3. algorithm:压缩算法/功能,默认 gzip,一般不做更改
        4. compressionOptions,对 algorithm 参数所选用的压缩功能的参数设置,一般用来设置压缩级别,1-9,数字越大,压缩后的大小越小,也越占用 CPU,花费时间也越长
        plugins: [
            new CompressionPlugin({
                compressionOptions: { level: 1 },
            })
        ],
        
        1. threshold:Number,设置被压缩资源的最小大小,单位为字节。默认为 0
        2. minRatio:Number,设置压缩比率,压缩比率 = 压缩后的资源的大小/压缩后的资源,小于压缩比率的资源才会被压缩。和 threshold 参数是‘与’的关系
        3. filename:类型:String|Function,设置压缩资源后的名称,默认值:[path].gz[query],[file]被替换为原始资产文件名。 [path]替换为原始资产的路径。 [dir]替换为原始资产的目录。 [name]被替换为原始资产的文件名。 [ext]替换为原始资产的扩展名。 [query]被查询替换
        new CompressionPlugin({
          filename(info) {
            console.log(info)
            return `${info.path}.gz${info.query}`
          },
        })
        
    4. NginxWebpack 压缩的区别

      • 不管 Nginx 还是 Webpack 压缩,在 Nginx 中都要开启 gzip 压缩,不然浏览器加载还是未压缩的资源。

        还可以在 Nginx 加上 gzip_static on;的配置。gzip_static 启用后, 浏览器请求资源时,Nginx 会先检查是否存该资源名称且后缀为.gz 的文件,如果有则直接返回该 gz 文件内容,可以避免 Nginx 对该资源再进行 gzip 压缩,浪费服务器的 CPU。

      • 用 Nginx 压缩会占用服务器的 CPU,浏览器每次请求资源,Nginx 会对该资源实时压缩,压缩完毕后才会返回该资源,如果资源很大的话,还是压缩级别设置很高,都会导致返回资源的时间过长,造成不好的用户体验。

      • 用 Webpack 会使打包时间变长。但是用 CompressionPlugin 插件压缩,会有缓存,可以相对减少打包时间。

      • 建议 Nginx 和 Webpack 压缩都开启压缩,且在 Nginx 加上 gzip_static on;的配置,减少服务器的 CPU 的使用,当然还是要根据项目的情况实际选择。

    5. nginx 配置 vue 项目缓存

      1. vue 的所有资源修改后打包出来的名称都会改变,所以可以使用强缓存,对 cssjspngttfjpg

        location ~* \.(css|js|png|jpg|jpeg|gif|gz|svg|mp4|ogg|ogv|webm|htc|xml|woff)$ {
            access_log off;
            add_header Cache-Control max-age=604800;
        }
        
      2. html 文件因为名称不会改变,所以使用 协商缓存,html 文件有改动就会立即更新, max-age=no-cache 代表进入协商缓存,文件改动会自动更新,不改动会返回 304

        location ~* \.(html)$ {
            access_log off;
            add_header  Cache-Control  max-age=no-cache;
        }
        

2. 常见的性能优化手段?

  1. 减少 http 请求
  2. 使用 http2
  3. 静态资源使用 CDN
  4. 将 CSS 放在文件头部,JavaScript 文件放在底部
  5. 使用字体图标 iconfont 代替图片图标
  6. 设置缓存,强缓存,协商缓存
  7. 压缩文件,css(MiniCssExtractPlugin),js(UglifyPlugin),html(html-webpack-plugin)文件压缩,清除无用的代码,tree-shaking(需要 es6 的 import 才支持),gzip 压缩(compression-webpack-plugin)
  8. splitChunks 分包配置,optimization.splitChunks 是基于 SplitChunksPlugin 插件实现的
  9. 图片优化、图片压缩
  10. webpack 按需加载代码,hash,contenthash

3. 骨架屏

4. 减少首屏渲染时间

5. 前端性能监控

前端监控:它指的是通过一定的手段来获取用户行为以及跟踪产品在用户端的使用情况,并以监控数据为基础,为产品优化指明方向,为用户提供更加精确、完善的服务。

前端监控 一般分为三大类:

  • 数据监控(监控用户行为)
    1. PV/UV:PV 即页面浏览器或点击率;UV 即访问某个站点或点击某条新闻的不同 IP 地址的人数
    2. 用户在每一个页面的停留时间
    3. 用户通过什么入口来访问该网页
    4. 用户在相应的页面中触发的行为...等
  • 性能监控(监控页面性能)
    1. 不同用户,不同机型和不同系统下的首屏加载时间
    2. 白屏时间
    3. http 等请求的响应时间
    4. 静态资源整体下载时间
    5. 页面渲染时间...等
  • 异常监控(监控产品、系统异常)
    1. JavaScript 的异常监控
    2. 样式丢失的异常监控...等

7. 前端性能埋点

  • 埋点的三种方法:

    1. 手动埋点(代码埋点):在需要埋点的业务逻辑功能位置调用接口,上报埋点数据(像友盟、百度统计、神策) 缺点:项目工程量大,需要埋点的位置太多
    2. 可视化埋点(通过可视化交互的手段,代替上述的代码埋点。将业务代码和埋点代码分离) 缺点:埋点的控件有限,不能手动定制
    3. 无埋点(前端自动采集全部事件,上报埋点数据,由后端来过滤和计算出有用的数据) 优点:前端只要一次加载埋点脚本;缺点:流量和采集的数据过于庞大,服务器性能压力大
  • 埋点上报的方式:

    1. 图片(优先考虑 GIF) 优点:防止跨域、防止阻塞页面加载,影响用户体验、相比 PNG/JPG,GIF 的体积最小

    大多采用的是 1*1 像素的透明 GIF 来上报

    1. Beacon 用于将数据异步发送到服务器。navigator.sendBeacon(url, data);