首屏优化

本文最后更新于:9 个月前

首屏优化

  首先祭出神图,来源是 w3.org

什么是首屏

什么是首屏呢?
  显而易见,这就是打开网址后最先出现在用户面前的页面,在这个首次展现的过程中,我们会遇到很多问题,比如说:输入网址后回车,页面是白色的,需要等待很久才能出来内容。
  那么为了解决这个问题,我们必须了解浏览器首屏的时候都做了什么事情?其实这里面涉及到很多的内容,因为我个人认为首屏时间其实包含用户从输入好网址后回车开始页面出现内容为止这段时间,那么这段时间发生了什么呢?

输入url到页面出来中发生了什么?

  我们只讨论其中比较重要的部分,那么显而易见的可以分成以下几个部分(详情可以看另一篇):

  • url解析,dns 查询

  • 连接请求

  • 响应并接受资源

  • 页面渲染

    在这里我们主要看看页面渲染过程中的各个性能指标是什么。

性能指标有哪些

  我们通过Chrome的控制台输入performance就可以输出performance.timing的值,从这里我们可以很清晰的看到神图中各个部分的执行时间。

w3.org 中可以读到这些参数的具体作用和使用流程:

  1. If the navigation is aborted for any of the following reasons, abort these steps without changing the attributes in window.performance.timing and window.performance.navigation.
    如果导航由于下列任何原因而中止,则中止这些步骤,但不要更改window.performance.timing和window.performance.navigation中的属性。
    • The navigation is aborted due to the sandboxed navigation browsing context flag or the sandboxed top-level navigation browsing context flag, or a preexist attempt to navigate the browsing context.
      由于沙箱导航浏览上下文标志或沙箱顶级导航浏览上下文标志,或预先存在的导航浏览上下文的尝试,导航将中止。
    • The navigation is caused by fragment identifiers within the page.
      导航是由页面内的片段标识符引起的。
    • The new resource is to be handled by some sort of inline content.
      新资源将由某种内联内容处理。
    • The new resource is to be handled using a mechanism that does not affect the browsing context.
      新资源将使用一种不影响浏览上下文的机制来处理。
    • The user refuses to allow the document to be unloaded.
      用户拒绝卸载文档
  2. Immediately after the user agent prompts to unload the previous document, record the current time as navigationStart.
    在用户代理提示卸载前一个文档之后,立即将当前时间记录为navigationStart。
  3. Record the current navigation type in window.performance.navigation.type if it has not been set:
    如果还没有设置,则在window.performance.navigation.type中记录当前的导航类型
    • If the navigation was started by clicking on a link, or entering the URL in the user agent’s address bar, or form submission, or initializing through a script operation other than the location.reload() method, let the navigation type be TYPE_NAVIGATE.
      如果导航是通过单击链接,或在用户代理的地址栏中输入URL,或表单提交,或通过location.reload()方法以外的脚本操作进行初始化来启动的,那么导航类型应该是TYPE_NAVIGATE
    • If the navigation was started either as a result of a meta refresh, or the location.reload() method, or other equivalent actions, let the navigation type be TYPE_RELOAD.
      如果导航是元刷新的结果,或者location.reload()方法,或者其他等效操作的结果,那么就让导航类型为TYPE_RELOAD。
    • If the navigation was started as a result of history traversal, let the navigation type be TYPE_BACK_FORWARD.
      如果导航是历史遍历的结果,请将导航类型设置为TYPE_BACK_FORWARD。
    • Otherwise, let the navigation type be TYPE_RESERVED.
      否则,让导航类型为TYPE_RESERVED。

The PerformanceNavigation interface

interface PerformanceNavigation {
  const unsigned short TYPE_NAVIGATE = 0;
  const unsigned short TYPE_RELOAD = 1;
  const unsigned short TYPE_BACK_FORWARD = 2;
  const unsigned short TYPE_RESERVED = 255;
  readonly attribute unsigned short type;
  readonly attribute unsigned short redirectCount;
};

  • TYPE_NAVIGATE
    • Navigation started by clicking on a link, or entering the URL in the user agent’s address bar, or form submission, or initializing through a script operation other than the ones used by TYPE_RELOAD and TYPE_BACK_FORWARD as listed below.
      导航开始于单击链接,或在用户代理的地址栏中输入URL,或表单提交,或通过脚本操作(TYPE_RELOAD和TYPE_BACK_FORWARD所使用的操作除外)进行初始化,如下所示。
  • TYPE_RELOAD
    • Navigation through the reload operation or the location.reload() method.
      通过重载操作或location.reload()方法进行导航。
  • TYPE_BACK_FORWARD
    • Navigation through a history traversal operation.
      通过历史记录遍历操作进行导航。
  • TYPE_RESERVED
    • Any navigation types not defined by values above.
      没有由上面的值定义的任何导航类型
  1. If the current document and the previous document are from different origins, set both unloadEventStart and unloadEventEnd to 0 then go to step 6. Otherwise, record unloadEventStart as the time immediately before the unload event.
    如果当前文档和前一个文档来自不同的起源,将unlodeventstart和unlodeventend都设置为0,然后执行步骤6。否则,将unloaddeventstart记录为立即发生卸载事件之前的时间。
  2. Immediately after the unload event is completed, record the current time as unloadEventEnd.
    在卸载事件完成后,立即将当前时间记录为unloaddeventend
  3. If the new resource is to be fetched using HTTP GET or equivalent, immediately before a user agent checks with the relevant application caches, record the current time as fetchStart. Otherwise, immediately before a user agent starts the fetching process, record the current time as fetchStart.
    如果要使用HTTP GET或同等的方法获取新资源,则在用户代理检查相关应用程序缓存之前,将当前时间记录为fetchStart。否则,在用户代理开始抓取过程之前,将当前时间记录为fetchStart。
  4. Let domainLookupStart, domainLookupEnd, connectStart and connectEnd be the same value as fetchStart.
    让domainLookupStart、domainLookupEnd、connectStart和connectEnd与fetchStart相同。
  5. If the resource is fetched from the relevant application cache or local resources, including the HTTP cache, go to step 13.
    如果是从相关应用缓存或本地资源(包括HTTP缓存)获取,请执行步骤13
  6. If no domain lookup is required, go to step 11. Otherwise, immediately before a user agent starts the domain name lookup, record the time as domainLookupStart.
    如果不需要进行域解析,执行步骤11。否则,在用户代理开始域名查找之前,将时间记录为domainLookupStart。
  7. Record the time as domainLookupEnd immediately after the domain name lookup is successfully done. A user agent may need multiple retries before that. If the domain lookup fails, abort the rest of the steps.
    域名查找成功后,立即将时间记录为domainLookupEnd。在此之前,用户代理可能需要多次重试。如果域查找失败,则中止其余步骤。
  8. If a persistent transport connection is used to fetch the resource, let connectStart and connectEnd be the same value of domainLookupEnd. Otherwise, record the time as connectStart immediately before initiating the connection to the server and record the time as connectEnd immediately after the connection to the server or the proxy is established. A user agent may need multiple retries before this time. If a connection can not be established, abort the rest of the steps.
    如果使用一个持久传输连接来获取资源,则让connectStart和connectEnd的值与domainLookupEnd相同。否则,在与服务器建立连接之前,将时间记录为connectStart;在与服务器或代理建立连接之后,将时间记录为connectStart。在此之前,用户代理可能需要多次重试。如果不能建立连接,则中止其余步骤。
  9. In step 11, a user agent should also carry out these additional steps if it supports the secureConnectionStart attribute:
    在第11步中,如果用户代理支持secureConnectionStart属性,它也应该执行这些额外的步骤:
    • If the scheme of the current document is HTTPS, the user agent must record the time as secureConnectionStart immediately before the handshake process to secure the connection.
      如果当前文档的方案是HTTPS,则用户代理必须在握手之前将时间记录为secureConnectionStart,以确保连接安全。
    • If the scheme of the current document is not HTTPS, the user agent must set the value of secureConnectionStart to 0.
      如果当前文档的方案不是HTTPS,则用户代理必须将secureConnectionStart的值设置为0。
  10. Immediately before a user agent starts sending request for the document, record the current time as requestStart.
    在用户代理开始发送文档请求之前,将当前时间记录为requestStart。
  11. Record the time as responseStart immediately after the user agent receives the first byte of the response.
    在用户代理接收到响应的第一个字节后,立即将时间记录为responseStart。
  12. Record the time as responseEnd immediately after receiving the last byte of the response.
    在接收到响应的最后一个字节后,立即将时间记录为responseEnd。
  13. If the fetched resource results in an HTTP redirect or equivalent, then
    如果获取的资源导致HTTP重定向或等效的结果,那么
    • if the current document and the document that is redirected to are not from the same origin, set redirectStart, redirectEnd, unloadEventStart, unloadEventEnd and redirectCount to 0. Then, return to step 6 with the new resource.
      如果当前文档和被重定向到的文档不是来自同一个来源,则设置redirectStart、redirectEnd、unlodeventstart、unlodeventend和redirectCount为0。然后,使用新资源返回到步骤6。
    • if there is previous redirect involving documents that are not from the same origin, set redirectStart, redirectEnd, unloadEventStart, unloadEventStart and redirectCount to 0. Then, return to step 6 with the new resource.
      如果先前的重定向涉及到来自不同来源的文档,则将redirectStart、redirectEnd、unlodeventstart、unlodeventstart和redirectCount设置为0。然后,使用新资源返回到步骤6。
    • Increment redirectCount by 1.
      redirectCount增量1。
    • If the value of redirectStart is 0, let it be the value of fetchStart.
      如果redirectStart的值为0,则设为fetchStart的值。
    • Let redirectEnd be the value of responseEnd.
      让redirectEnd作为responseEnd的值。
    • Set all the attributes in window.performance.timing to 0 except navigationStart, redirectStart, redirectEnd, unloadEventStart and unloadEventEnd.
      将window.performance.timing中除navigationStart、redirectStart、redirectEnd、unlodeventstart和unlodeventend之外的所有属性设置为0。
    • Return to step 6 with the new resource.
      使用新资源返回到步骤6。
  14. Record the time as domLoading immediately before the user agent sets the current document readiness to “loading”.
    在用户代理将当前文档准备状态设置为“loading”之前,将时间记录为domLoading。
  15. Record the time as domInteractive immediately before the user agent sets the current document readiness to “interactive”.
    在用户代理将当前文档准备状态设置为“交互式”之前,将时间记录为domInteractive。
  16. Record the time as domContentLoadedEventStart immediately before the user agent fires the DOMContentLoaded event at the document.
    在用户代理在文档中触发DOMContentLoaded事件之前,将时间记录为domContentLoadedEventStart。
  17. Record the time as domContentLoadedEventEnd immediately after the DOMContentLoaded event completes.
    在DOMContentLoaded事件完成后,将时间记录为domContentLoadedEventEnd。
  18. Record the time as domComplete immediately before the user agent sets the current document readiness to “complete”.
    在用户代理将当前文档准备就绪状态设置为“完成”之前,将时间记录为domComplete。
  19. Record the time as loadEventStart immediately before the user agent fires the load event.
    在用户代理触发load事件之前,将时间记录为loaddeventstart。
  20. Record the time as loadEventEnd immediately after the user agent completes the load event.
    在用户代理完成load事件后,立即将时间记录为loaddeventend。

  从上面的官方文字我们可以看出来timing指标中主要涉及三个方面:

  • 网络请求时间
    redirectEnd redirectStart domainLookUpEnd domainLookUpStart connectEnd connectStart
  • 请求响应时间
    requestStart responseEnd responseStart
  • 解析渲染时间
    domLoading domInteractive domContentLoadedEventStart domContentLoadedEventEnd domComplete

  其中可以算出一些常用的性能指标:

  • DNS查询耗时 = domainLookupEnd - domainLookupStart
  • TCP链接耗时 = connectEnd - connectStart
  • request请求耗时 = responseEnd - responseStart
  • 解析dom树耗时 = domComplete - domInteractive
  • domready时间 = domContentLoadedEventEnd - fetchStart
  • 首屏渲染时间、首次有内容渲染时间 performance.getEntriesByType(‘paint’) https://w3c.github.io/paint-timing/
  • onload时间 = loadEventEnd - fetchStart

    可以做什么来优化

    常见的几种SPA首屏优化方式:
  1. 各种资源的压缩:
    • 减小资源
      • 减小入口文件积
        通过懒加载的方式引入文件
      • 静态资源本地缓存
        存储这方面有很多方式,一个是请求方面的的缓存机制,一种是使用work进行缓存,还有日常的本地化缓存
        • http缓存
          http缓存是一种保存资源副本并在下次请求的时候直接根据情况来使用该副本的技术。如果发现有缓存,就会拦截请求直接返回该资源的拷贝,免去了再次下载资源。有强缓存和协商缓存两种。可以参考浏览器缓存
        • service worker离线缓存
          service worker是一个服务器与浏览器之间的代理人,通过他去判断哪些资源需要在加载。或者某些时候我们可以通过这个后台脚本实现后台同步内容和推送通知的操作。可以参考service Worker
        • localstorage等
      • UI框架按需加载
        在引用库时,我们可以根据情况只引入部分我们需要的组件,这样可以避免引入整个库文件。将外部资源通过cdn的方式进行引入,又能减少打包体积又能加速请求。
      • 图片资源的压缩
        直接对图片资源进行压缩,有很多方法可以直接压缩图片体积,然后针对图标内容,可以将图标合成到一张图片上,然后在使用时只需要使用该图片并且调整到目标图表上进行展示即可。有很多在线使用的工具可以合成雪碧图并且生成图标对应的css样式。
    • 组件重复打包
      如果使用的是webpack,那么webpack的SplitChunks插件就支持分包然后共享。具体可以直接看官网split-chunks-plugin
    • 开启GZip压缩
      我们在拆包之后也可以使用compression-webpack-plugin压缩一下,同时在服务器端也是需要配置gzip的,如果是nginx可以直接在配置文件中配置gzip:on,如果是别的框架也可以使用对应的插件实现。
  2. 使用SSR
    ssr(server side rendering),是服务器完成页面的HTML结构拼接然后发送至浏览器的服务器渲染技术。vue直接用nuxt.js去搭建就好了
  3. 页面渲染性能优化
  • 优化html代码
    css放在顶部先加载,js放在底部后加载,减少DOM的总数
  • 优化js、css会改变dom树的文件
    使用worker把重任务分离出去,避免影响dom,减少重排重绘的发生,使css易于载入
  • 优化动画
    使用requestAnimationFrame来进行动画绘制,使用transform和opacity来实现一些简单的动画效果
    页面渲染发生了什么

    怎么监控性能

    获取performance.timing中的值,在使用img或者sendBean的方式进行上报。或者在某些需要自定义的位置使用performance.mark进行标记,并且使用performance.measure来计算时间差来进行上报。
  • sendBeacon
    使用 sendBeacon() 方法会使用户代理在有机会时异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能。这就解决了提交分析数据时的所有的问题:数据可靠,传输异步并且不会影响下一页面的加载。
    window.addEventListener('unload', logData, false);
    function logData() {
        navigator.sendBeacon("/request", analyticsData);
    }
  • img
    通过img发送GET请求,也可以实现日志上报,但是业务代码不能写在里面,因为图片可能会被拦截。
    let imgReq = new Image();
    imgReq.onload = imgReq.onerror = imgReq.onabort = function () {
        //请求结果
    }
    imgReq.src = url;

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!