1、字体优化

1.1 字体包压缩

使用 fontmin 这个库压缩字体包。(字体下载地址 字体下载网 ,这个下载的苹方简-常规可以被格式化。)

const Fontmin = require('fontmin')

const srcPath = './path/*.ttf' // 字体源文件
const destPath = './path' // 输出路径
const text = 'text'

var fontmin = new Fontmin()
  .src(srcPath)
  .dest(destPath)
  .use(
    Fontmin.glyph({
      text: text,
      hinting: false // keep ttf hint info (fpgm, prep, cvt). default = true
    })
  )

// 执行
fontmin.run(function (err, files, stream) {
  if (err) {
    console.error(err) // 异常捕捉
  }
  console.log('done') // 成功
})

1.2 优化字体包加载前的文本隐藏 [from font-display 的用法]

  1. @font-face 中使用 font-display 属性
/* style.css */
@font-face {
  font-family: 'test'; /* 所指定的字体名字将会被用于font或font-family属性 */
  /* 
    src描述符指定了包含字体数据的资源。
    远程字体文件位置的URL或者用户计算机上的字体名称,
    可以使用local语法通过名称指定用户的本地计算机上的字体。
    如果找不到该字体,将会尝试其他来源,直到找到它。
  */
  src: url('test.woff2') format('woff2');
  font-display: swap;
}
  1. 考虑兼容性,使用 js 判断字体包加载完毕后,添加字体设置。
// font.js
  if ('fontDisplay' in document.body.style === false) {
    if ('fonts' in document) {
      document.fonts.load('test')
      document.fonts.ready.then((fontFaceSet) => {
        document.documentElement.className += ' fonts-loaded' 
      })
    }
  }

1.3 字体包 preload

添加 preload-webpack-plugin 包

$ yarn add preload-webpack-plugin --dev

在 vue.config.js 中配置

// vue.config.js
const PreloadWebpackPlugin = require('preload-webpack-plugin')

module.exports = {
  configureWebpack: config => {
    return {
      plugins: [new PreloadWebpackPlugin({
        rel: 'preload',
        include: 'allAssets',
        as (entry) { // link as 属性
          if (/\.ttf$/.test(entry)) return 'font'
          if (/\.jpg$/.test(entry)) return 'image'
          return 'script'
        },
        fileWhitelist: [/\.ttf/, /banner[^-]*\.jpg/] // 白名单
      })]
    }
  }
}

相关知识补充

  • font-display font-display MDN

    font-display属性必须在@font-face指令内使用。因为你不能控制第三方字体供应商的CSS文件,所以你没有办法控制font-display属性更用说给他传递值了。 决定了一个@font-face 在不同的下载时间和可用时间下是如何展示的。

    字体显示时间轴

    字体显示时间线基于一个计时器,该计时器在用户代理尝试使用给定下载字体的那一刻开始。时间线分为三个时间段,在这三个时间段中指定使用字体的元素的渲染行为。

    • 字体阻塞周期 如果未加载字体,任何试图使用它的元素都必须渲染不可见的后备字体。如果在此期间字体已成功加载,则正常使用它。

    • 字体交换周期 如果未加载字体,任何尝试使用它的元素都必须呈现后备字体。如果在此期间字体已成功加载,则正常使用它。

    • 字体失败周期 如果未加载字体,用户代理将其视为导致正常字体回退的失败加载。

    属性值

    • auto 默认值,字体显示策略由用户代理定义。自定义字体的文本会被先隐藏,直到字体加载结束才会显示。
    • block 为字体提供一个短暂的阻塞周期和无限的交换周期。
    • swap 为字体提供一个非常小的阻塞周期和无限的交换周期。后备文本立即显示直到自定义字体加载完成后再使用自定义字体渲染文本。
    • fallback 为字体提供一个非常小的阻塞周期和短暂的交换周期。需要使用自定义字体渲染的文本会在较短时间内不可见,如果自定义字体还没加载结束,就先加载无样式文本。一旦自定义字体加载结束,那么文本就会被正确赋予样式。
    • optional 为字体提供一个非常小的阻塞周期,并且没有交换周期。效果和fallback几乎一样,不过 optional 可以让浏览器自由决定是否使用自定义字体,很大程度上取决于浏览器的连接速度。如果速度很慢,那自定义字体将不会被使用。
  • @font-face 中 src 属性的 format 函数

    format() 函数描述该URL引用的字体资源的格式的可选提示。 格式提示包含逗号分隔的格式字符串列表,这些字符串表示众所周知的字体格式。 如果用户代理不支持指定的格式,它将跳过下载字体资源。如果没有提供格式提示,则始终下载字体资源。

  • document.documentElement

    对于任何非空 HTML 文档,调用 document.documentElement 总是会返回一个 <html> 元素,且它一定是该文档的根元素。借助这个只读属性,能方便地获取到任意文档的根元素。 HTML 文档通常包含一个子节点 ,但在它前面可能还有个 DOCTYPE 声明。XML 文档通常包含多个子节点:根元素,DOCTYPE 声明,和 processing instructions。

    所以,应当使用 document.documentElement 来获取根元素, 而不是 document.firstChild。

  • document.fonts

    document.fonts 返回文档的 FontFaceSet 接口。FontFaceSet 接口对于加载新字体,检查先前加载的字体的状态等非常有用。

    属性 (不全面)

    • FontFaceSet.status 指示字体的加载状态。将是 ‘loading’ or ‘loaded’。
    • FontFaceSet.ready Promise 会在字体加载和布局操作完成后 resolves。

    方法 (不全面)

    • FontFaceSet.check() 返回一个布尔值,指示字体是否加载,但未加载时不启动加载。

    • FontFaceSet.load() FontFaceSet.load() MDN 该方法强制加载参数中给定的所有字体。返回一个Promise,该Promise解析为所请求字体的字体数组。

      参数

      • font: 参照 css 语法中 font 属性的格式。eg: ‘italic bold 16px Roboto’
      • text: 限制 font faces 编码范围至少要包含的字符。
  • <link ref="preload" ... > 参考文档

2、图片使用懒加载

使用 vue-lazyload 库优化图片加载,滚动到某处再加载图片。

3、打包线上版本,使用 cdn 引入包

// --------------
// .env
// --------------
NODE_ENV === 'production'

// --------------
// vue.config.js
// --------------

const isProd = process.env.NODE_ENV === 'production'

const assetsCDN = {
  // webpack build externals
  externals: {
    vue: 'Vue',
    'vue-router': 'VueRouter',
    'vue-lazyload': 'VueLazyload',
  },
  css: ['//cdn.jsdelivr.net/npm/animate.css@3.7.2/animate.min.css'],
  js: [
    '//cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js',
    '//cdn.jsdelivr.net/npm/vue-router@3.1.6/dist/vue-router.min.js',
    '//cdn.jsdelivr.net/npm/vue-lazyload@1.3.3/vue-lazyload.min.js',
  ],
}

module.exports = {
  configureWebpack: (config) => {
    if (isProd) {
      return {
        // resolve: {
        //   alias: {
        //     '@ant-design/icons/lib/dist$': resolve('./src/core/antd/icons.js'),
        //   },
        // },
        externals: assetsCDN.externals,
        // plugins: [new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)],
      }
    }
  },

  chainWebpack: (config) => {
    config.plugin('html').tap((args) => {
      args[0].title = process.env.VUE_APP_TITLE
      if (isProd) {
        args[0].cdn = assetsCDN
      }
      return args
    })
  },
}
<!-- index.html -->
...
<!-- require cdn assets css -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
<% } %> 
...
<!-- require cdn assets js -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
<script
  type="text/javascript"
  src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"
></script>
<% } %>

4、ant-design-vue 相关

4.1 打包后,icon 包体积过大

参考此 issue 作者的回答

4.2 打包后,moment 包体积过大

参考此优化方案 github

5、使用缓存

5.1 nginx 配置 last-modify 和 etag

location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|ttf)$
{
    expires      60d;
}
location ~ .*\.(js|css)?$
{
    expires      60d;
}

6、组件懒加载

6.1 使用 Intersection Observer 实现组件懒加载

主要使用了 Intersection Observer (MDN) 接口,提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。

// test.vue
<template>
  // ...
  <component-a v-if="ifCmpShow" ></component-a>
</template>
<script>
export default {
  data () {
    return {
      ifCmpShow: false
    }
  },
  mounted () {
    // 这里如果不等dom更新后执行,页面加载时就会触发 observe
   if ('IntersectionObserver' in window) {
      this.$nextTick(() => {
        this.lazyLoadContact()
      })
    } else {
      this.$nextTick(() => {
        this.ifCmpShow = true
      })
    }
  },
  method: {
    lazyLoadContact () {
      const intersectionObserver = new IntersectionObserver(entries => {
        if (entries[0].intersectionRatio <= 0) return
        if (!this.ifCmpShow) {
          this.ifCmpShow = true
        }
      })

      intersectionObserver.observe(this.$refs.experts.$el) // 参数必须是 dom
    }
  }
}
</script>

相关知识点

以上。