Web性能知识(三)

Author Avatar
GeniusFunny 2月 26, 2020
  • 在其它设备中阅读本文章

Web性能取决于:资源获取【网络I/O】 + 页面布局与渲染 【浏览器渲染原理】+ JavaScript执行【事件循环】

这是一篇讲解性能优化手段的文章,参考后续补上;内容都是自己的笔记

网络优化

预加载技术

Preload

<link rel="preload">用于强制浏览器请求资源并且不会阻塞onload事件

Preload用于提前加载当前页面所需的资源,例如脚本、图片之类

Prefetch

<link rel="prefetch"用于暗示浏览器此资源可能需要,但是什么时候加载取决于浏览器。

Prefetch用于提前准备接下来访问的页面需要的资源,例如:在访问A页面时提前加载B页面的资源,导航到B页面时B页面加载更快

Preoload、Prefetch网络优先级

img

对于script,不同的位置以及是否是defer、async、blocking都影响着其网络优先级:

  • blocking && 在第一张图片前,Medium
  • blocking && 在第一张图片后,Low
  • async/defer/插入的脚本,Lowest

对于Image:

  • 可见且位于可视区域,Medium
  • 不在可视区域,Lowest
  • 布局完成后,低优先级且未加载的图片进入视口时会提升优先级

Preload的as属性给予的优先级与<link type>一致:

  • <link rel="preload" as="style" href="xx.css">,Highest
  • 都受到CSP约束
  • 不带as的Preload等同于async XHR

Prefetch资源时跳转到其他页面时,该请求不会停止,且会在网络堆栈中保留5分钟

Preload Header不会等待浏览器扫描HTML发现需要的prelaod资源而是直接加载;与此同时使用Preload首部可能会触发HTTP/2 PUSH

页面加载优化

优化内容效率

  • 基于文本的资源优化
    • 预处理与环境特定优化
      • 删除无关注释
      • 简化CSS样式
      • 删除无用的空格和制表符、未使用的代码
    • 根据不同内容采取不同压缩算法
      • 启用GZIP压缩CSS、HTML、JS
  • 图像优化
    • 消除或替换图像
      • CSS3实现渐变阴影等
      • 网页字体代替图片字体
    • 选择合适的格式
      • 矢量图
        • 适合包含几何图形的形状
        • 缩放和分辨率无关
        • 使用点、线、多边形来表示图像
      • 光栅图
        • 应用于包含大量不规则形状和复杂场景
        • 对矩形格栅内的每个像素的值进行编码来表示图像
    • 优化矢量图(SVG)
      • 缩减SVG文件(删除各种元数据)
      • SVG文件应通过Gzip压缩来减小传输体积
    • 优化光栅图
      • 每个像素都包含了颜色和透明度信息,RGBA
      • 图像压缩程序使用各种方法来减少每个像素所需的位数,以减小图像的文件大小
    • 无损图像压缩 vs 有损图像压缩
      • 图像格式上的差异源于压缩算法的差别
      • 有损压缩:去掉某些像素数据
      • 无损压缩:对像素数据进行压缩

优化图片

JPEG压缩算法
  • 基线压缩算法
    • 从上到下进行编码解码
  • 渐进式压缩算法
    • 多次扫描逐渐提高图像质量
    • 网速差时给用户展示质量差的图像,提高感知性能
    • 解码速度比基线JPEG慢2-3倍,小型图片上体积大于基线JPEG
  • 无损压缩算法
    • 通过移除EXIF数据、优化图像的霍夫曼表、重新扫描图像来优化图像
WebP压缩算法
  • 有损压缩算法
    • 体积比JPEG小25%-34%
  • 无损压缩算法
    • 比PNG小26%
优化手段
  • 使用srcset提供HiDPI图像,允许浏览器为每个设备选择最佳图像

    1
    2
    3
    4
    <img srcset="paul-irish-320w.jpg,
    paul-irish-640w.jpg 2x,
    paul-irish-960w.jpg 3x"
    src="paul-irish-960w.jpg" alt="Paul Irish cameo">
  • 图像精灵

    • 不适用于HTTP/2,因为现在可在单个连接中多次请求
  • 延迟加载非关键图像

    • 加快首页加载速度
    • 减少数据消耗、降低电池消耗
    • Lazysizes
    • 缺点:
      • 屏幕阅读器等禁用script无法看到图片
      • 滚动侦听器会对浏览器滚动性能产生影响(可能导致多次重绘)
  • 避免diplay: none问题

    • 给图片设置display:none时,也会触发图像src请求
    • 使用<picture><img srcset>来解决问题
  • 图像上CDN、HTTP缓存

  • 预加载关键图片

  • 渐进式图像

优化JavaScript

存在的问题
  • 网络:
    • 仅发送用户所需代码
      • 代码拆分,将代码拆分为关键、非关键代码
      • 延迟加载非关键代码
    • 源码压缩:uglifyJS压缩源码
    • 压缩:Gzip
    • 移除未使用的代码:tree shake、代码覆盖率检查
    • 缓存
  • 解析/编译/执行时间/内存GC/长任务
PRPL(推送、渲染、预先缓存、延迟加载)

img

Tree Shaking
Code Splitting

存在的问题:JS文件过大,导致首次加载页面缓慢且部分模块更新时必须重新整体打包发布,未能更好利用缓存

拆分方式:

  • Vendor拆分,将公共代码拆分出来【所有应用必须实现】
  • 入口拆分,按应用程序的入口进行拆分
  • 动态拆分,使用import()语句进行代码拆分【适用于SPA】

字体优化

字体加载顺序:

  • 浏览器执行页面布局并确定需要使用何种字体
  • 对于所需字体,浏览器确认是存在本地
  • 对于不存在本地的字体,浏览器按照格式顺序进行加载
    • 若浏览器不支持第一种格式,则加载第二种格式
    • 若浏览器支持则下载字体
优化加载和渲染

加载存在的问题(延迟文本渲染):

  • 浏览器请求HTML文档
  • 浏览器解析HTML响应、构建DOM
  • 浏览器发现 CSS、JS 以及其他资源并分派请求
  • 浏览器在收到所有 CSS 内容后构建 CSSOM,然后将其与 DOM 树合并以构建渲染树。
    • 在渲染树指示需要哪些字体变体在网页上渲染指定文本后,将分派字体请求。
  • 浏览器执行布局并将内容绘制到屏幕上
    • 如果字体尚不可用,浏览器可能不会渲染任何文本像素。
    • 字体可用之后,浏览器将绘制文本像素。

解决方案:

  • 使用字体unicode子集
  • 压缩字体:GZIP
  • 预加载字体<link rel="preload" as="font" href="">
  • font-display
    • auto:使用用户代理的字体策略,多为block
    • block:3s未加载则回退字体,字体加载后交换
    • swap:首先用默认字体,字体加载后交换
    • fallback:100ms内未加载字体则回退,3s内加载了字体则交换
    • optional:100ms内未加载字体则回退
  • FontLoading API:提供一种脚本编程接口来定义和操纵 CSS 字体,追踪其下载进度,以及替换其默认延迟下载行为

懒加载资源

延迟加载是指延迟加载页面中的非关键资源,例如:位于可视区域外的图片、视频等。

图像延迟加载
  • 根据srcoll事件,判断元素是否进入可视区域【如何判断元素进入可视区域

    • getBoundingClientRect返回的是元素相对于视口的位置;加上scrollX/scrollY就是元素的在网页的位置。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      document.addEventListener("DOMContentLoaded", function() {
      let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
      let active = false;

      const lazyLoad = function() {
      if (active === false) {
      active = true; // 每200ms执行一次,可以用函数节流代替

      setTimeout(function() {
      lazyImages.forEach(function(lazyImage) {
      if ((lazyImage.getBoundingClientRect().top <= window.innerHeight && lazyImage.getBoundingClientRect().bottom >= 0) && getComputedStyle(lazyImage).display !== "none") {
      lazyImage.src = lazyImage.dataset.src;
      lazyImage.srcset = lazyImage.dataset.srcset;
      lazyImage.classList.remove("lazy");

      lazyImages = lazyImages.filter(function(image) {
      return image !== lazyImage;
      });

      if (lazyImages.length === 0) {
      document.removeEventListener("scroll", lazyLoad);
      window.removeEventListener("resize", lazyLoad);
      window.removeEventListener("orientationchange", lazyLoad);
      }
      }
      });

      active = false;
      }, 200);
      }
      };

      document.addEventListener("scroll", lazyLoad);
      window.addEventListener("resize", lazyLoad);
      window.addEventListener("orientationchange", lazyLoad);
      });
  • 使用Intersection ObserverAPI

    • 由浏览器提供的API,性能和效率更高

    • 分离检测元素是否在可视区域的代码,开发者只需关注 元素进入/离开的业务

    • 由于浏览器差异,部分旧的浏览器版本不支持(兼容性差)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      document.addEventListener("DOMContentLoaded", function() {
      var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));

      if ("IntersectionObserver" in window) {
      let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
      if (entry.isIntersecting) {
      let lazyImage = entry.target;
      lazyImage.src = lazyImage.dataset.src;
      lazyImage.srcset = lazyImage.dataset.srcset;
      lazyImage.classList.remove("lazy");
      lazyImageObserver.unobserve(lazyImage);
      }
      });
      });

      lazyImages.forEach(function(lazyImage) {
      lazyImageObserver.observe(lazyImage);
      });
      } else {
      // Possibly fall back to a more compatible method here
      }
      });
视频延迟加载
  • 视频不自动播放
  • 用视频替代动画gif

代码执行优化

requestAnimationFrame、requestIdleCallback

  • requestAnimationFrame可以合适地调度动画,使渲染尽可能达到60fps
  • requestIdleCallback利用空闲阶段(每一帧的空余时间或用户不活跃时)调度任务
requestIdleCallback

A typical frame

requestIdleCallback(myNonEssentialWork)

  • 第一个参数为回调函数

    • 该回调函数会收到一个deadline作为其实参

      1
      2
      3
      4
      5
      6
      function myNonEssentialWork(deadline) {
      while(deadline.timeRemaining() > 0 && tasks.length > 0) // 若有空闲时间则调度任务
      doWorkIfNeeded();
      if (tasks.length > 0)
      requestIdleCallback(anontherNonEssentialWork) // 上一个任务执行完毕,可调度其他任务(若还有时间)
      }
  • 第二个参数为options对象,{ timeout: xxx }

    • 确保在所给定的时间内,该任务一定会执行(用于浏览器一直忙碌的情形)
    • 该回调函数收到的deadline.timeout就是该参数
  • 返回一个ID

    • 可被取消,window.cancelIdleCallback(handle)
requestAnimationFrame

requestAnimationFrame告诉浏览器在下次重绘前调用指定的回调函数更新动画。

业界常见性能优化手段

Google PageSpeed Insights Rules

  • 消除阻塞渲染的JavaScript和CSS(最大限度减少网页上关键资源的数量并尽可能消除这些资源,减少下载关键字节数,优化关键路径长度)
  • 优化JavaScript的使用(JavaScript资源会阻塞解析器,所以将其标记为async或通过专门的JavaScript代码进行添加。阻塞解析器的JavaScript强制浏览器等待CSSOM并暂停DOM的构建,继而大大延迟首次渲染的时间
  • 延迟解析JavaScript(延迟加载对构建首次渲染的可见内容无关紧要的脚本)
  • 避免运行时间长的JavaScript
  • 优化CSS的使用(CSS置于文档head标签内、避免使用CSS import、内联阻塞渲染的CSS)

Yahoo Best Practices for Speeding Up Your Web Site

  • 减少http请求
  • 使用CDN
  • 利用浏览器缓存
  • 压缩页面元素
  • 样式表放在head
  • JavaScript文件放在底部
  • 避免CSS表达式
  • JS、CSS放在外部文件中
  • 减少CDN查询
  • 压缩JavaScript
  • 避免重定向

现代前端的优化策略

  • 前端资源文件离线化
  • 页面组件化并按需加载
  • 预渲染提升感官性能