改进 Nuxt TTFB

Posted

技术标签:

【中文标题】改进 Nuxt TTFB【英文标题】:Improve Nuxt TTFB 【发布时间】:2020-10-13 13:10:37 【问题描述】:

我正在使用 Nuxt 和 Vuetify 构建一个大型应用程序,一切都很好并且工作正常,但不幸的是,Lighthouse 的分数并不是最好的,只有 42 分的性能。

我已经改进了一些东西,例如:

从 google 加载更好的字体; 将异步代码从 nuxtServerInit 移动到布局; 删除不必要的第三方服务;

从 42 到 54,但我仍然对结果不太满意。

很遗憾,由于我缺乏知识,我在这些改进方面做得不是最好的。

我看到TTFB 根本不是最佳的,但我真的不知道我可以改进什么......所以我希望你能通过提示和建议帮助我改进我的应用程序。

我将在这里粘贴我的nuxt.congig.js,以便您了解我在使用什么以及如何使用:

const path = require('path')
const colors = require('vuetify/es5/util/colors').default
const bodyParser = require('body-parser')

const maxAge = 60 * 60 * 24 * 365 // one year
const prefix = process.env.NODE_ENV === 'production' ? 'example.' : 'exampledev.'
const description =
  'description...'

let domain
if (
  process.env.NODE_ENV === 'production' &&
  process.env.ENV_SLOT === 'staging'
) 
  domain = 'example.azurewebsites.net'
 else if (
  process.env.NODE_ENV === 'production' &&
  process.env.ENV_SLOT !== 'staging'
) 
  domain = 'example.com'
 else 
  domain = ''


module.exports = 
  mode: 'universal',

  /**
   * Disabled telemetry
   */
  telemetry: false,

  /*
   ** Server options
   */
  server: 
    port: process.env.PORT || 3030
  ,

  serverMiddleware: [
    bodyParser.json( limit: '25mb' ),
    '~/proxy',
    '~/servermiddlewares/www.js'
  ],

  router: 
    middleware: 'maintenance'
  ,

  env: 
    baseUrl:
      process.env.NODE_ENV === 'production'
        ? 'https://example.com'
        : 'https://localhost:3030',
    apiBaseUrl:
      process.env.API_BASE_URL || 'https://example.azurewebsites.net'
  ,

  /*
   ** Headers of the page
   */
  head: 
    title: 'example',
    meta: [
       charset: 'utf-8' ,
       name: 'viewport', content: 'width=device-width, initial-scale=1' ,
      
        hid: 'description',
        name: 'description',
        content: description
      ,
      
        hid: 'fb:app_id',
        property: 'fb:app_id',
        content: process.env.FACEBOOK_APP_ID || 'example'
      ,
      
        hid: 'fb:type',
        property: 'fb:type',
        content: 'website'
      ,
      
        hid: 'og:site_name',
        property: 'og:site_name',
        content: 'example'
      ,
      
        hid: 'og:url',
        property: 'og:url',
        content: 'https://example.com'
      ,
      
        hid: 'og:title',
        property: 'og:title',
        content: 'example'
      ,
      
        hid: 'og:description',
        property: 'og:description',
        content: description
      ,
      
        hid: 'og:image',
        property: 'og:image',
        content: 'https://example.com/images/ogimage.jpg'
      ,
      
        hid: 'robots',
        name: 'robots',
        content: 'index, follow'
      ,
      
        name: 'msapplication-TileColor',
        content: '#ffffff'
      ,
      
        name: 'theme-color',
        content: '#ffffff'
      
    ],
    link: [
      
        rel: 'apple-touch-icon',
        sizes: '180x180',
        href: '/apple-touch-icon.png?v=GvbAg4xwqL'
      ,
      
        rel: 'icon',
        type: 'image/png',
        sizes: '32x32',
        href: '/favicon-32x32.png?v=GvbAg4xwqL'
      ,
      
        rel: 'icon',
        type: 'image/png',
        sizes: '16x16',
        href: '/favicon-16x16.png?v=GvbAg4xwqL'
      ,
       rel: 'manifest', href: '/site.webmanifest?v=GvbAg4xwqL' ,
      
        rel: 'mask-icon',
        href: '/safari-pinned-tab.svg?v=GvbAg4xwqL',
        color: '#777777'
      ,
       rel: 'shortcut icon', href: '/favicon.ico?v=GvbAg4xwqL' ,
      
        rel: 'stylesheet',
        href:
          'https://fonts.googleapis.com/css?family=Abril+Fatface|Raleway:300,400,700&display=swap'
      
    ]
  ,

  /*
   ** Customize the page loading
   */
  loading: '~/components/loading.vue',

  /*
   ** Global CSS
   */
  css: ['~/assets/style/app.scss', 'swiper/dist/css/swiper.css'],

  /*
   ** Plugins to load before mounting the App
   */
  plugins: [
    '@/plugins/axios',
    '@/plugins/vue-swal',
    '@/plugins/example',
     src: '@/plugins/vue-infinite-scroll', s-s-r: false ,
     src: '@/plugins/croppa', s-s-r: false ,
     src: '@/plugins/vue-debounce', s-s-r: false ,
     src: '@/plugins/vue-awesome-swiper', s-s-r: false ,
     src: '@/plugins/vue-html2canvas', s-s-r: false ,
     src: '@/plugins/vue-goodshare', s-s-r: false 
  ],

  /*
   ** Nuxt.js modules
   */
  modules: [
    '@/modules/static',
    '@/modules/crawler',
    '@nuxtjs/axios',
    '@nuxtjs/auth',
    '@nuxtjs/device',
    '@nuxtjs/prismic',
    '@dansmaculotte/nuxt-security',
    '@nuxtjs/sitemap',
    [
      '@nuxtjs/google-analytics',
      
        id: 'example',
        debug: 
          sendHitTask: process.env.NODE_ENV === 'production'
        
      
    ],
    ['cookie-universal-nuxt',  parseJSON: false ],
    'nuxt-clipboard2'
  ],

  /*
   ** Security configuration
   */
  security: 
    dev: process.env.NODE_ENV !== 'production',
    hsts: 
      maxAge: 15552000,
      includeSubDomains: true,
      preload: true
    ,
    csp: 
      directives: 
        // removed contents
      
    ,
    referrer: 'same-origin',
    additionalHeaders: true
  ,

  /*
   ** Prismic configuration
   */
  prismic: 
    endpoint: 'https://example.cdn.prismic.io/api/v2',
    preview: false,
    linkResolver: '@/plugins/link-resolver',
    htmlSerializer: '@/plugins/html-serializer'
  ,

  /*
   ** Auth module configuration
   */
  auth: 
    resetOnError: true,
    localStorage: false,
    cookie: 
      prefix,
      options: 
        maxAge,
        secure: true,
        domain
      
    ,
    redirect: 
      callback: '/callback',
      home: false
    ,
    strategies: 
      local: 
        endpoints: 
          login: 
            url: '/auth/local',
            method: 'POST',
            propertyName: 'token'
          ,
          logout:  url: '/auth/logout', method: 'POST' ,
          user:  url: '/me', method: 'GET', propertyName: false 
        ,
        tokenRequired: true,
        tokenType: 'Bearer'
      ,
      google: 
        client_id:
          process.env.GOOGLE_CLIENT_ID ||
          'example'
      ,
      facebook: 
        client_id: process.env.FACEBOOK_APP_ID || 'example',
        userinfo_endpoint:
          'https://graph.facebook.com/v2.12/me?fields=about,name,pictureurl,email',
        scope: ['public_profile', 'email']
      
    
  ,

  /*
   ** Vuetify Module initialization
   */
  buildModules: [
    ['@nuxtjs/pwa',  meta: false, oneSignal: false ],
    '@nuxtjs/vuetify'
  ],

  /*
   ** Vuetify configuration
   */
  vuetify: 
    customVariables: ['~/assets/style/variables.scss'],
    treeShake: true,
    rtl: false,
    defaultAssets: 
      font: false,
      icons: 'fa'
    
  ,

  /*
   ** Vue Loader configuration
   */
  chainWebpack: config => 
    config.plugin('VuetifyLoaderPlugin').tap(() => [
      
        progressiveImages: true
      
    ])
  ,

  /*
   ** Build configuration
   */
  build: 
    analyze: true,
    optimizeCSS: true,
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) 
      config.resolve.alias.vue = 'vue/dist/vue.common'
      // Run ESLint on save
      if (ctx.isDev && ctx.isClient) 
        config.devtool = 'cheap-module-source-map'
        config.module.rules.push(
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/,
          options: 
            fix: true
          
        )
      
      if (ctx.isServer) 
        config.resolve.alias['~'] = path.resolve(__dirname)
        config.resolve.alias['@'] = path.resolve(__dirname)
      
    
  

一些可能有用的信息:

我只为每个页面和组件使用范围样式,而且自定义样式的数量非常少,因为我几乎使用了 Vuetify 的所有内容; 当我从浏览器“查看页面源代码”时,我不希望在页面内看到很长的 CSS,而不是最小化... 我不使用fetchasyncData 加载任何内容,我更喜欢在安装组件后加载数据; Evrything 部署在 Azure 上,我使用 .Net 核心 API。

很高兴知道一些最佳实践以及一些改进性能的示例,尤其是 TTFB。

在 Lighthouse 中,我看到“删除未使用的 JavaScript”,其中包含 /_nuxt/.. 文件列表...但我不认为这些文件未使用,所以我想知道它们的原因像这样标记...

也许 Azure 应该在每次部署时清理项目?我不知道... 我使用az Azure Cli 并通过git push azure master 进行部署,所以没什么特别的。

"减少初始服务器响应时间"...如何?在 Azure 中运行生产应用程序的计划更快,我应该改进什么以及如何改进?

最小化主线程工作”:这是什么意思?

减少 JavaScript 执行时间”:如何?

我希望你能帮助我理解和提升一切。

我会根据您的要求更新这篇文章,也许您希望看到更多关于该项目的信息。谢谢

【问题讨论】:

【参考方案1】:

我最近不得不通过一个相当大的 Nuxt 应用程序来完成这个过程,所以我可以分享一些我们提出的见解和解决方案。在我们开心之前,我们设法提高了大约 40 分。

我对任何阅读者的第一条建议:放弃框架。按照设计,它们很臃肿,可以处理尽可能多的常见用例,并使应用程序尽可能简单,但会牺牲大小。在浏览器领域,大小和速度就是一切,每个新框架(Nuxt、Vue、Vuetify)都增加了另一个抽象层,这会对大小和速度产生负面影响。

不管怎样,除此之外,对于那些不能放弃框架的人来说,这里有一些其他的建议。

灯塔经常会产生误导

我们发现“删除未使用的 javascript”警告基本上无法用 Vue 修复。问题是 Lighthouse 只能检查在测试期间实际运行的代码,并且不知道在 Vue 运行时中用于错误处理或 onclick 处理的代码是必要的,直到它当然是。

不幸的是,无法提前知道运行时中需要哪些代码,因此需要全部发送。但是,作为开发人员,您至少可以控制在应用程序初始加载期间需要哪些第三方库、模块和插件。您可以确保只发送和使用必要的部分。

所以在 Lighthouses 眼中,有很多无用的、未使用的代码。然而,一旦应用程序需要做任何事情,它就不再无用了。因此,为什么它有些误导。

请始终牢记这一点,因为这些工具会报告很多“问题”,而这只是 Javascript 应用程序如何工作的事实。在我看来,这些框架的开发人员似乎还有一些障碍需要克服,才能让 Javascript 应用在 Google 眼中真正易于访问和高性能。

保持你的插件和模块简短。

您在nuxt.config.js 中添加到应用程序的每个插件都会增加每个页面中包含的主 JS 包的大小。这不可避免地会导致大量未使用的代码、巨大的 JS 文件大小,当然还有更长的加载时间。

只在需要它们的页面上添加插件是完全有效的:

// inside the SocialSharing.vue component
import Vue from 'vue'
import VueGoodshare from 'vue-goodshare'
Vue.use(VueGoodshare)

export default  ... 

提醒:发生此导入的页面仍将添加来自vue-goodshare 的所有代码。最好只包含这些库中您实际需要的组件。

检查这一点的一个好方法是将analyze 属性设置为true 来运行您的构建。 (在这里分享您的分析可能会对您有所帮助)

Reduce Initial Server Response Time

如果您已经在运行最好的服务器,您仍然可以做一些事情来帮助加快速度。

    为您的页面利用缓存,因此无需在服务器端呈现它们。但是,其中一些测试(如 Lighthouse)专门禁用缓存,导致结果不佳。 减少渲染页面所需的工作量。确保没有阻塞 API 调用发生,保持页面简单和小巧,并确保服务器没有过载。 利用边缘缓存或边缘部署,使您的应用程序更接近您的用户。例如,如果您的应用程序部署在 USWEST,而 Lighthouse 正在迪拜进行测试,您可能会在该请求中看到很多延迟,这将延长服务器响应时间。

您可能需要跟进您正在运行的特定服务器及其所在位置以获得更多帮助。但是,我概述的要点几乎肯定会让您的 TFFB 获得绿色分数。

Minimize Main Thread Work

在浏览器中,主线程是所有动作发生的地方。它完全负责处理用户交互、更新页面,并且实质上是将 HTML 文档变成一个活的应用程序。过于繁忙的主线程可能会导致性能问题,尤其是在用户尝试与您的页面交互时会引起他们的注意。

通常,当你看到这个时,这是因为你运行了太多的 Javascript。具体来说,您一次运行了太多 Javascript,最终阻塞了主线程。大量使用 Javascript 的应用程序因此而臭名昭著,这可能是一个非常难以解决的问题。

对我们的应用来说最大的帮助是延迟加载不重要的脚本。例如,我们在所有页面上运行 Rollbar 和 Google Analytics。我们不是在应用程序启动时加载脚本,而是只加载它们的小命令队列,并将大脚本的加载时间延迟约 5 秒。这释放了主线程来专注于更重要的事情,例如为 Vue 应用程序补充水分。

您还可以通过减少要处理的 JS 数量来节省大量成本。返回给客户端的每一行代码都是必须发送、解析和执行的另一行代码。我肯定会先看看你的modulesplugins,看看有没有容易实现的目标。

Reduce Javascript Execution Time

这是另一个令人遗憾的指标,在​​我们的测试中通常只是意味着“应用仍在做某事”。我说这很不幸,因为根据我们的经验,它不会影响应用程序的性能或用户体验。

我们经常看到我们的第三方服务,如 Intercom、Rollbar、GA 等,将它们的执行时间延长到 10 秒以上,而使用第三方代码,除了不使用它,你无能为力。

我的建议:专注于使用我强调的所有其他内容来优化应用程序。这是很难专门修复的问题,通常只是其他问题的症状,例如主线程太忙,第三方代码很慢。

最后一条建议

如果所有其他方法都失败了,您也许可以“欺骗”一些对您有利的测试。为此,我们将 GA 和 Rollbar 脚本的加载延迟到测试完成之后。请记住,此工具会查看特定时间范围内的特定指标,并据此对您进行评分。您或许可以利用简单的替代技术(例如首屏下的延迟加载)来观察性能的显着差异。

无论如何,这是一项相当复杂的任务,这里绝不会有“成功的三步指南”。你会在网上找到很多指南,声称他们通过一些简单的更改将 Vue 应用程序从 30 个提升到 100 个,但他们都忽略了这样一个事实,即真正的应用程序有很多代码并做了很多事情,并且平衡了这一点速度和性能是一种艺术形式。

您可能想查看the shell application model 或service workers 等资源。

如果您需要对这篇文章进行任何澄清,请随时提出。但是请记住,您要问的问题很广泛,并且不仅仅是一种“正确”的接近方式。最终由你来决定这里的重要部分并尽可能地应用它们。

更新示例

我所讨论的大部分内容都很难举例说明,因为我所讨论的主题要么过于简单化且不需要解释,要么一开始就包含模糊的概念。但是,可以展示我们使用的一种取得了不错结果的方法。

这是我们用来加载对讲机的修改脚本示例:

    var APP_ID = "your_app_id_here";
    window.intercomSettings = 
        app_id: APP_ID,
        hide_default_launcher: !0,
        session_duration: 36e5
    ,
    function() 
        var n,
            e,
            t = window,
            o = t.Intercom;
        "function" == typeof o ? (o("reattach_activator"), o("update", t.intercomSettings)) : (n = document, (e = function() 
            e.c(arguments)
        ).q = [], e.c = function(t) 
            e.q.push(t)
        , t.Intercom = e, o = function() 

            // Don't load the full Intercom script until after 10s
            setTimeout(function() 
                var t = n.createElement("script");
                t.type = "text/javascript",
                t.crossorigin = "anonymous",
                t.async = !0,
                t.src = "https://widget.intercom.io/widget/" + APP_ID;
                var e = n.getElementsByTagName("script")[0];
                e.parentNode.insertBefore(t, e)
            , 1e4)

        , "complete" === document.readyState ? o() : t.attachEvent ? t.attachEvent("onload", o) : t.addEventListener("load", o, !1))

这是他们为您提供的脚本的自定义版本,您可以将其放入您的应用程序<head></head> 标记中。但是,您会注意到我们添加了一个setTimeout 函数,该函数将延迟完整对讲脚本的加载。这使您的应用程序有机会加载其他所有内容,而无需竞争网络或 CPU 时间。

但是,由于不再保证对讲机可用,因此在与它交互时需要更加小心。

这个完全相同的概念几乎可以应用于您可能加载的每个第 3 方脚本。我们还将它与 Google Analytics 一起使用,我们在其中初始化命令队列,但延迟加载实际脚本。显然,这可能会导致短会话出现跟踪问题,但如果性能是您的主要目标,这是您需要做出的权衡。

【讨论】:

非常感谢您的友好回复。我会处理您的所有建议,但我有几个问题:1)如何在 nuxt 组件中加载插件并解决“未定义窗口”问题?我试了几次,都没有成功。 2)你怎么能延迟谷歌分析nuxt模块?再次感谢! @AyeyeBrazo 关于 1),我相信有两种方法可以解决这个问题。挂载钩子仅称为客户端,因此您可以动态导入通常在挂载钩子中使用的代码,或者使用 来防止特定的 Vue 组件被 s-s-r 处理。至于 2),有一个配置选项可以禁用初始脚本加载,但您可能想在 repo 上打开一个问题来询问这个问题。 你好@HMilbradt 很高兴在你的回复中看到一些关于你的解决方案的代码示例。谢谢 我有点晚了,但我添加了一个我们为加载对讲机而修改的脚本示例。不幸的是,我们已经修改了缩小的代码,所以它有点复杂,但它应该足以让大体思路通过。这可以应用于从 Rollbar 到 GA 的所有内容,它肯定会影响 Lighthouse 如何看待我们的表现。

以上是关于改进 Nuxt TTFB的主要内容,如果未能解决你的问题,请参考以下文章

前端周报:微软发布基于Chromium的Microsoft Edge预览版;Nuxt发布v2.9.0;npm 发布v6.11.0

如何改进SVM算法,最好是自己的改进方法,别引用那些前人改进的算法

YOLOv5改进YOLOv7改进|YOLO改进超过50种注意力机制,全篇共计30万字(内附改进源代码),原创改进50种Attention注意力机制和Transformer自注意力机制

什么是 TTFB, 为什么 TTFB 很重要

什么是 TTFB, 为什么 TTFB 很重要

关于改进建议几个方面的有效实践