web 性能优化

TIP

HTML、javaScript、CSS、图片等静态资源本质上都从服务器上获取。可服务器响应客户端请求是需要时间的,返回的资源体积越大,耗时越长。所以想要快,有三方面考虑

减少资源体积大小

codeSplit

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效。更详细的可以参考VueRouteropen in new window官网的的配置。 vue 代码分割的实现(codesplit),做了代码分割后,会将代码分离到不同的 bundle 中,然后进行按需加载这些文件,能够提高页面首次进入的速度,网站性能也能够得到提升 1、VueRouter 路由按需加载

// 将 加载路由组件方式
import HelloWorld from '@/components/HelloWorld'
// 改为 路由懒加载模式
const HelloWorld = () => import('@/components/HelloWorld')

2、组件中按需加载其他组件 将普通记载方式

import vOther from '@/components/other'
export default {
  components: {
    vOther
  }
}

改为懒加载模式

const vOther = () => import('@/components/other')

Tree Shaking

Tree Shaking 指的是引入一个模块的时候,不引入这个模块的所有代码,只引入需要的代码,这就需要借助 webpack 里面自带的 Tree Shaking 这个功能,帮助我们实现。Tree Shaking 只支持 ES Module(import....) 不支持 require....

优化 webpack 打包

CDN 加载资源

较大、较多的图片等资源放 CDN,这也是变相的减小了包体积。代码里直接引用 CDN 的链接,更有利于程序的加载速度

TIP

CDN 的全称是 Content Delivery Network,即内容分发网络。CDN 是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率

CDN 解决的问题

  • 打包时间太长、打包后代码体积太大,请求慢
  • 服务器网络不稳带宽不高,使用 cdn 可以回避服务器带宽问题

使用步骤

  1. 在项目根目录 index.html 使用 cdn 节点导入
<body>
  <div id="app"></div>
  <!-- built files will be auto injected -->
  <!--开发环境-->
  <script src="https://cdn.bootcss.com/vue/2.6.11/vue.js"></script>
  <!--生产环境-->
  <!--<script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>-->
  <!-- 引入组件库 -->
  <script src="https://cdn.bootcss.com/vue-router/3.2.0/vue-router.min.js"></script>
  <script src="https://cdn.bootcss.com/axios/0.23.0/axios.min.js"></script>
  <script src="https://cdn.bootcss.com/element-ui/2.15.6/index.js"></script>
  <script src="https://cdn.bootcdn.net/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
</body>
  1. 在 vue.config.js 中加入 externals 外部扩展

    TIP

    externals 配置选项的作用:引用一个库,但是又不想让 webpack 打包,并且又不影响我们在程序中以 CMD、AMD 或者 window/global 全局等方式进行使用,那就可以通过配置 externals

configureWebpack: {
    externals: {
        "vue": "Vue",
        "vue-router": "VueRouter",
        "axios": "axios",
        "moment": "moment",
        "element-ui": "ELEMENT",
    }
},
  1. vue-cli 4 使用 report 分析 vendor.js Vue Cli(@/vue/cli)自带的 webpack 包体积优化工具,它可以查看各个模块的 size 大小,方便优化。只需要在 build 后面加上 --report 参数即可。

    • 我们把命令配置到 package.json 里
    "scripts": {
      "serve": "vue-cli-service serve",
      "build": "vue-cli-service build",
      "report": "vue-cli-service build --report"  //加入该行
    },
    

    执行 npm run report 打包并生成 report,运行 npm run report 后,会在 build 的同时,在 dist 目录会生成一个 report.html,打开后可看到各文件占用大小

PurgeCSS

在 vue 项目中使用 删除未使用的 CSS 代码

在提高 web 性能的方法中,可以通过移除不需要的 js 和 css 达到性能提高。 PurgeCSS 是一个用来删除未使用的 CSS 代码的工具 PurgeCSSopen in new window

  1. 安装
vue add @fullhuman/purgecss

安装成功后会在项目创建postcss.config.js文件

  1. postcss.config.js 文件修改
  • content 字段,来告诉 PurgeCSS 去哪里查找将要对应匹配的 class
  • whitelist 对于一些你不想要移除的 class 或者某些标签上对应的样式名称,你可以它们加到白名单字段中。你至少需要添加 html 和 body 标签以及任意的动态 class 样式名称到白名单配置字段中
const IN_PRODUCTION = process.env.NODE_ENV === 'production'

module.exports = {
  plugins: [
    IN_PRODUCTION &&
      require('@fullhuman/postcss-purgecss')({
        content: [
          `./public/**/*.html`,
          `./src/**/*.vue`,
          `./layouts/**/*.vue`,
          `./components/**/*.vue`,
          `./views/**/*.vue`
        ],
        whitelist: ['html', 'body'],
        defaultExtractor(content) {
          const contentWithoutStyleBlocks = content.replace(
            /<style[^]+?<\/style>/gi,
            ''
          )
          return (
            contentWithoutStyleBlocks.match(
              /[A-Za-z0-9-_/:]*[A-Za-z0-9-_/]+/g
            ) || []
          )
        },
        safelist: [
          /-(leave|enter|appear)(|-(to|from|active))$/,
          /^(?!(|.*?:)cursor-move).+-move$/,
          /^router-link(|-exact)-active$/,
          /data-v-.*/
        ]
      })
  ]
}

加载资源的优先级

例如首屏,只需要优先渲染可视区的内容,非关键的延后加载即可。 关键数据可以让 native 进行预请求,再将关键数据直接交给 h5 即可

缓存

http 缓存 本地缓存。例如 native 对图片资源进行缓存 接口缓存。

加载方式

懒加载

骨架屏 了解一下。

预加载

NSR 了解一下。

缓存加载

CSR 了解一下

离线包

webview 优化

并行初始化

所谓并行初始化,是指用户在进入 App 时,系统就创建 WebView 和加载模板,这样 WebView 初始化和 App 启动就可以并行进行了,这大大减少了用户等待时间。

资源预加载。资源预加载,是指提前在初始化的 WebView 里面放置一个静态资源列表,后续加载东西时,由于这部分资源已经被强缓存了,页面显示速度会更快。那么,要预加载的静态资源一般可以放:

一定时间内不变的外链; 一些基础框架,多端适配的 JS(如 adapter.js),性能统计的 SDK 或者第三方库(如 react/vue.js); 基础布局的 CSS 如 base.css。

其它。如何 native 提供了接口请求的 API,那针对接口请求也可做优化。

ps: 别小瞧 webview 这些,做好了能给你减少 100-200ms 的时间。

小结一下

APP 启动阶段的优化方案

webview 优化

页面白屏阶段的优化方案

离线化设计,例如离线包 骨架屏 SSR NSR

首屏渲染阶段的优化方案

优化请求数量

三、项目图片懒加载

一次性加载大量图片的时候,容易造成网络资源浪费,同时也可能造成服务器卡顿。当浏览器滚动的时候,对滚动事件进行监听,当图片距离视口的顶部大于浏览器可见视口的宽度的时,图片不加载,这样就可以减少网络资源的浪费。采用 vue-lazyloadopen in new window组件进行实现。

安装组件
yarn add vue-lazyload
main.js 设置
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload)
Vue.use(VueLazyload, {
  preLoad: 1.3,
  error: 'dist/error.png',
  loading: 'dist/loading.gif',
  attempt: 1
})
系统中使用
<img v-lazy="img.src" />

CSS Sprites 精灵图

使用字体图标

TIP

在 vue 项目中使用

  1. 矢量图标下载
  2. 解压后将字体图标文件夹放到项目 assets/iconfont 文件夹下
  3. main.js 中引入 import './assets/iconfont/index.css'
  4. 在项目中使用<span class="icon-bookmark"></span>

合并 JS 和 CSS 文件

利用浏览器缓存

接口预加载

DOM 性能优化

长列表优化。 memo、usememo 减少 Render 次数 js 计算优化

性能平台 https://juejin.cn/post/6971359910525141005#heading-12

打包开启 gzip 压缩

安装依赖文件

cnpm install webpack@4.46.0 terser-webpack-plugin@4.2.3 compression-webpack-plugin@6.1.1 -D

vue.config.js 设置 gzip

const path = require('path')

// gzip 压缩
const CompressionPlugin = require('compression-webpack-plugin')
const productionGzipExtensions = /\.(js|css|json|txt|ico|svg)(\?.*)?$/i
// 处理 js 的压缩和混淆
const TerserPlugin = require('terser-webpack-plugin')

function resolve(dir) {
  return path.join(__dirname, dir)
}

// vue.config.js
module.exports = {
  /*
    Vue-cli3:
    Crashed when using Webpack `import()` #2463
    https://github.com/vuejs/vue-cli/issues/2463
   */
  // 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
  productionSourceMap: false,
  configureWebpack: (config) => {
    //生产环境取消 console.log
    if (process.env.NODE_ENV === 'production') {
      config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true
    }
    // 处理 js 的压缩和混淆
    config.optimization = {
      minimize: process.env.NODE_ENV === 'production',
      minimizer: [
        new TerserPlugin({
          test: /\.js(\?.*)?$/i, // 匹配参与压缩的文件
          parallel: true, // 使用多进程并发运行
          terserOptions: {
            // Terser 压缩配置
            output: { comments: false }
          },
          extractComments: false // 将注释剥离到单独的文件中
        })
      ]
    }
  },

  chainWebpack: (config) => {
    if (process.env.NODE_ENV === 'production') {
      // 压缩
      config.plugin('compressionPlugin').use(
        new CompressionPlugin({
          filename: '[path].gz[query]',
          algorithm: 'gzip',
          test: productionGzipExtensions,
          threshold: 10240,
          minRatio: 0.8
          // deleteOriginalAssets: true
        })
      )
    }

    config.resolve.alias
      .set('@$', resolve('src'))
      .set('@api', resolve('src/api'))
      .set('@assets', resolve('src/assets'))
      .set('@comp', resolve('src/components'))
      .set('@views', resolve('src/views'))
      .set('@layout', resolve('src/layout'))
      .set('@static', resolve('src/static'))
  },
  css: {
    loaderOptions: {
      less: {
        modifyVars: {
          /* less 变量覆盖,用于自定义 ant design 主题 */
        },
        javascriptEnabled: true
      },
      sass: {
        data: `@import "@/assets/sass/tk-scss-variable.scss";`
      }
    }
  },

  devServer: {
    port: 3099,
    proxy: {
      '/bh-sim': {
        target: 'http://localhost:9099',
        ws: false,
        changeOrigin: true
      }
    }
  },

  lintOnSave: undefined
}

nginx 开启 gzip

 server {
	#端口号,不同的程序,复制时,需要修改其端口号
        listen      5099;
	#服务器地址,可以为IP地址,本地程序时,可以设置为localhost
        server_name  localhost;
        client_max_body_size 2G;

    # 开启gzip
        gzip on;
    # 启用gzip压缩的最小文件,小于设置值的文件将不会压缩
        gzip_min_length 1k;
    # gzip 压缩级别,1-9,数字越大压缩的越好,也越占用CPU时间,后面会有详细说明
        gzip_comp_level 1;
    # 进行压缩的文件类型。javascript有多种形式。其中的值可以在 mime.types 文件中找到。
        gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png application/vnd.ms-fontobject font/ttf font/opentype font/x-woff image/svg+xml;
    # 是否在http header中添加Vary: Accept-Encoding,建议开启
        gzip_vary on;
    # 禁用IE 6 gzip
        gzip_disable "MSIE [1-6]\.";
    # 设置压缩所需要的缓冲区大小
        gzip_buffers 32 4k;
    # 设置gzip压缩针对的HTTP协议版本
        gzip_http_version 1.0;

	#程序所在目录
        root D:/workSpace/pack/pack_jar/bh-sim/frontEnd;
        charset utf-8;
            index index.html;

        location / {
            try_files $uri $uri/ /index.html;
        }

        location @rewrites {
            rewrite ^(.+)$ /index.html last;
        }

	#程序映射地址,将【tick-ehs】改为你程序名称,将【proxy_pass】 改为你自己的后台地址
        location /bh-sim {
            proxy_pass http://localhost:9099/bh-sim;
            proxy_cookie_path / /bh-sim;
        }
    }

javaScript 小技巧

使用文档碎片减少 DOM 操作

<ul id="list"></ul>
const list = document.getElementById('list')
// 文档碎片
// createdocumentfragment()方法创建了一虚拟的节点对象,节点对象包含所有属性和方法。
// 当你想提取文档的一部分,改变,增加,或删除某些内容及插入到文档末尾可以使用createDocumentFragment() 方法。
// 减少了对Dom的操作
const fragment = document.createDocumentFragment()
for (let i = 0; i < 6; i++) {
  const item = document.createElement('li')
  item.innerHTML = `项目${i}`
  // list.appendChild(item)
  fragment.appendChild(item)
}
list.appendChild(fragment)

获取 URL 参数并转换为对象

一、创建函数并导出

/**
 * @desc 获取URL传递参数并转换为对象
 */
export function getQueryParams() {
  const result = {}
  const querystring = window.location.search
  const reg = /[?&][^?&]+=[^?&]+/g
  const found = querystring.match(reg)
  if (found) {
    found.forEach((item) => {
      let temp = item.substring(1).split('=')
      let key = temp[0]
      let value = temp[1]
      result[key] = value
    })
  }
  return result
}

二、 使用 URL 传参:http://localhost:1222/?name=zhangsan&sex=12

import { getQueryParams } from '@/utils/url.js'
export default {
  mounted() {
    const urlParams = getQueryParams()
    console.log(urlParams, 'urlParams') // {name: "zhangsan", sex: "12"}
  }
}

使用 Promise 加载图片

一、创建函数

/**
 * @desc 使用 Promise 加载图片
 * @param src 图片路径
 */
export function loadImage(src) {
  const promise = new Promise((resolve, reject) => {
    const img = document.createElement('img')
    img.onload = function () {
      resolve(img)
    }
    img.onerror = function () {
      const error = new Error(`图片加载失败,url为:${src}`)
      reject(error)
    }
    img.src = src
  })
  return promise
}

二、使用

import { loadImage } from '@/utils/utlis.js'
export default {
  name: 'home',
  mounted() {
    let url = 'https://image/home.png'
    loadImage(url)
      .then((res) => {
        console.log(res, 'res')
      })
      .catch((e) => {
        console.error('error', e)
      })
  }
}