前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >记一次 Nuxt.js 登录页性能优化

记一次 Nuxt.js 登录页性能优化

作者头像
尹光耀
发布2021-10-19 10:13:34
9410
发布2021-10-19 10:13:34
举报
文章被收录于专栏:前端小馆前端小馆

前言

最近有测试和 local 投诉,我们后台管理系统的登录页面经常要加载很久,常常会出现页面已经显示出来了,但是点击登录毫无反应,直到全部加载后才能登录。于是,他们提出让我们去优化。

这是一个挺好的问题,登录页虽然不是移动端那种首页,但也是最先呈现给内部用户的。

定位耗时

遇到这种问题,首先需要找出耗时都花在了哪里,然后再去想具体办法去解决。首先,打开登录页面控制面板,Disable Cache 之后查看一下每个资源的耗时。

从图上可以明显看出来,有一个 2.2m 的文件足足耗时 5s 之久,刷新了很多次后耗时都是在 4s - 5s 之间,而文件的耗时主要在下载上面,看来主要的性能瓶颈就在这里了。 由于 JS 文件在腾讯云 CDN 上面配置了协商缓存(etag),所以在第二次加载的时候速度提升非常大,基本上不到 1s 就可以加载出来了。

那么这个大文件是什么文件呢?我去 Jenkins 上看一下构建记录,在 build 的时候看到这个文件就是基于第三方包打出来的 vendors 文件。

webpack4 splitChunks

既然知道这个是 vendors 文件了,那就来分析一下 webpack 构建。

在 webpack4 里面出现了 splitChunk 来拆分 chunk 文件,webpack4 会有一个默认的 vendors chunk,它会把 node_modules 都给打成一个包,类似于:

代码语言:javascript
复制
optimization: {
    splitChunks: {
      chunks: 'initial',
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        }
      }
    }
  }

只不过,Nuxt 在这个基础上又拆分出了一个 commons ,配置规则如下:

代码语言:javascript
复制
optimization.splitChunks.cacheGroups.commons = {
  test: /node_modules[\\/](vue|vue-loader|vue-router|vuex|vue-meta|core-js|@babel\/runtime|axios|webpack|setimmediate|timers-browserify|process|regenerator-runtime|cookie|js-cookie|is-buffer|dotprop|nuxt\.js)[\\/]/,
  chunks: 'all',
  priority: 10,
  name: true
}

priority 代表优先级,如果两个 cacheGroups 里面都引用了同一个库,那么就根据优先级来判断优先把这个库打进哪个 chunk 里面。

很明显 commons 的优先级要高于 vendors,所以会把 test 规则匹配到的第三方包优先拆分出来,这几个主要是 Nuxt 中依赖的一些库。

本地执行了一次 analyze 后,得到的构建图是这样的,可以看出来 vendors 明显远比其他的包都要大,尤其是 xlsx、iview、moment、lodash 这几个库,几乎占了一大半体积。

优化

生成多 HTML

既然知道 vendors 包里面都是一些第三方库了,那么是否可以只打出登录页依赖的第三方库,然后只去加载这个 chunk 文件呢?

我看了一下登录页逻辑很简单,不需要 lodash、moment,甚至连 iview 都不需要,完全可以自己去实现样式,这样就不必去加载体积这么大的 vendors chunk 了。

真是个好主意,可是问题来了,怎么才能不去加载 vendors 呢?如果是在 webpack 里面,这个很容易,我们可以通过 html-webpack-plugin 来加载多个 HTML 文件,针对登录页生成一个 HTML 文件,让它只去加载自身依赖的 chunk 文件。

于是我去看了一下 Nuxt 源码,发现这里还是暴露了配置给我们去定义一个新的 HTML 模板的。当然,到最后我也没去尝试这种方法,只是觉得应该可以实现。

从 HTML 模板中删除

Nuxt 会暴露给我们一个 app.html 模板文件,它会在服务端渲染出来数据,最后替换到这个文件里面。

代码语言:javascript
复制
<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
  <head {{ HEAD_ATTRS }}>
    {{ HEAD }}
  </head>
  <body {{ BODY_ATTRS }}>
    {{ APP }}
  </body>
</html>

那么我们有没有可能在 Nuxt 替换这些占位符之前先去除掉不需要加载的 chunk 文件呢?其实也是可以的,只是需要修改到 Nuxt 的源码。

修改了源码之后,还需要用 patch-package 去打一个补丁,这样就可以做到修改 node_modules 里面的代码。

打开项目的 node_modules 文件夹,找到 @nuxt/vue-renderer/dist/vue-renderer.js,在 SSRRenderer 这个类里面的 render 方法中,我们可以看到如下代码:

m.script.text({ body: true }) 这句代码拿到的就是最后页面上渲染出来的 script 标签,如果在这里匹配到 vendors 包,把它给排除掉,之后在页面上就不会加载这个 JS 文件了。

我这里的方案是这样的,首先把登录页不需要且体积很大的几个包(iview、moment、lodash)给单独打了一个 my-vendors 的包,在 Nuxt 源码中用正则表达式去匹配这个文件名,然后手动 replace 掉(记得要把 link 标签里面预加载的也一起替换掉)

代码语言:javascript
复制
// nuxt.config.js
config.optimization.splitChunks.cacheGroups.myVendors = {
          test: /node_modules[\\/](view-design|moment|moment-timezone|dayjs|crypto-js|simple-uploader\.js|vue2-google-maps|vuex-class|axios)[\\/]/,
          // cacheGroupKey here is `commons` as the key of the cacheGroup
          automaticNamePrefix: 'my-vendors', // 文件名以 my-vendors 为前缀
          name: true,
          chunks: 'all',
          priority: 10
          reuseExistingChunk: true
}

// vue-renderer.js
const scripts = APP.match(/(\<script[\s\S]*?>[\s\S]*?\<\/script\>)/g) || []
const script = scripts.find(s => s.indexOf('my-vendors') > -1);
APP = APP.replace(script, '')
const links = HEAD.match(/(\<link rel="preload" [\s\S]*?>)/g) || []
const link = links.find(s => s.indexOf('my-vendors') > -1);
HEAD = HEAD.replace(link || "", '')

最终的效果的确是不会加载这个文件了,但是点击事件失效了,对比前后两次加载的文件,差别只有这个 my-vendors.js,不清楚为什么点击事件失效,所以最终为了赶时间也就没使用这个方法。

服务端直出

除了上面两种方式之外,还有一种比较简单的方式。由于 Nuxt 本身就会启动一个服务,官方也支持我们使用 express\koa 等等来实现服务端的路由,所以我们可以把登录页面直接用纯服务端渲染,去掉所有不必要的第三方库和文件。

涉及到图片之类的,我事先把他们上传到了 CDN 上面,然后根据环境变量去加载不同的 CDN 地址。

代码语言:javascript
复制
// login/template.ts
export default (config) => {
  return `
    <html>
      <head>
        <title data-n-head="true">AirPay Admin</title>
        <meta data-n-head="true" charset="utf-8">
        <meta data-n-head="true" name="viewport" content="width=device-width, initial-scale=1">
        <meta data-n-head="true" data-hid="description" name="description" content="Admin">
        <link data-n-head="true" rel="icon" type="images/x-icon" href="/favicon.ico">
        <link data-n-head="true" rel="preconnect" href="${
          config.cdnServer.staticUrl
        }" crossorigin="true">
        <link data-n-head="true" rel="preconnect" href="${
          config.apiServer.baseUrl
        }" crossorigin="true">
        <style>
          article, aside, blockquote, body, button, dd, details, div, dl, dt, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, hr, input, legend, li, menu, nav, ol, p, section, td, textarea, th, ul {
            margin: 0;
            padding: 0;
          }
          img {
            border-style: none;
          }
        </style>
        <script src="https://cdn.jsdelivr.net/npm/particles.js@2.0.0/particles.min.js"></script>
      </head>
      <body>
        <div id="particles" class="particles"></div>
        <div onclick="login()" class="login">
          <div class="login-content">
            <div style="width:350px" class="login-wrapper">
              <div class="login-title">
                <p
                  slot="title"
                  style="text-transform: capitalize; color: #595d65; font-size: 16px; display: flex; height: 25px;"
                >
                  <img src="${
                    config.cdnServer.staticUrl
                  }/static/admin-website/logo.png" alt="logo" class="login-logo" />
                </p>
              </div>
              <div class="login-body">
                <div class="login-button" onclick="login()">
                  <div class="login-icon"></div>
                  <span class="login-text">Sign in with Google</span>
                </div>
              </div>
            </div>
          </div>
        </div>
        <script>
          particlesJS('particles', {
            "particles": {},
            "interactivity": {}
            }
          }, function() {
            console.log('callback - particles.js config loaded');
          });
        </script>
        <script>
          function login() {}
        </script>
      </body>
    </html>
  `
}

然后,在 /login 路由下面引入这个模块,传入必要的配置后直接输出,记得设置 Content-Typetext/html

代码语言:javascript
复制
// login/index.ts
module.exports = function(
  fastify: Fastify.FastifyInstance,
  opts: Fastify.RouteShorthandOptions,
  next: Function
) {
  fastify.get('/login', async (request, reply) => {
    reply
      .code(200)
      .header('Content-Type', 'text/html; charset=utf-8')
      .send(login(Config))
  })
  next()
}

// server/index.ts
fastify.register(require('./routes/login'), { prefix: '/' })

最后优化的效果也是非常明显的,不使用缓存的情况下耗时只有几百毫秒。

在开启了缓存之后,几乎是秒开,耗时只有短短 100ms,可以说性能得到了几十倍的提升。

总结

很多时候我们总会抱怨自己的工作都是重复劳动,找不到可以提升自己的地方。但如果用心去找,还是能发现团队开发中的不少痛点的,尤其是一些大家都知道,但没人去解决或者解决不了的问题,自己在解决这些痛点的时候也能学习到很多新知识。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 定位耗时
  • webpack4 splitChunks
  • 优化
    • 生成多 HTML
      • 从 HTML 模板中删除
        • 服务端直出
        • 总结
        相关产品与服务
        内容分发网络 CDN
        内容分发网络(Content Delivery Network,CDN)通过将站点内容发布至遍布全球的海量加速节点,使其用户可就近获取所需内容,避免因网络拥堵、跨运营商、跨地域、跨境等因素带来的网络不稳定、访问延迟高等问题,有效提升下载速度、降低响应时间,提供流畅的用户体验。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档