[译]Base64 编码性能

news/2024/7/4 3:04:10 标签: 网络, 开发工具, 运维

原文作者为Harry,分为两部分:Part1 & Part2。

Part1: Base64有什么用?

减少请求数量,是这几年的一个优秀性能建议。虽然如此,也不是说它就没有缺陷。为了使页面加载更快,我们实际上可以通过高效的传输静态资源来实现,而不只是减少几个请求。

其中一个从减少请求数量诞生并被推崇的实践是使用Base64编码:将外部资源(e.g. 图片)直接嵌入到使用它的文本(e.g.样式表)中。减少HTTP请求数量的关键是,所有资源(样式表或图片)能够在同一时间到达。听起来像做梦,是吧?

然而并不是。

不幸的是,使用Base64编码是一个反模式[注1]。我希望在这篇文章中去分享关于关键路径优化,Gzip,当然还有Base64的一些思考。

我们来看一些代码

我写这篇文章是因为我刚刚为客户做了一个审计,遇到了下面要讨论到的问题。这是一个来自实际客户端的实际样式表:信息是匿名的,但这是一个完全真实的项目。

我在页面上运行了一个快速的网络配置文件,发现了一个样式表(某方面来说,这是件好事,因为我们是绝对不愿意看到有12个样式表请求的),但是这个样式表在解压缩之后居然有925K。实际请求到的字节少得多,但还是有232K。

当我们看到这么大体积的样式表时,开始感到恐慌了。我相当确定,甚至都不用去看,里面肯定有Base64。当然,并不是说它是唯一的原因(插件,缺乏结构,继承等等,都可能有影响),但这么大体积的样式表通常都是因为Base64。并且:

  • 不管是不是因为Base64,925K的样式表都很恐怖

  • 压缩也只能减少到759K

  • Gzip压缩到232K,去除了693K相同的代码

  • 232K的请求还是很恐怖

请注意,光是解析这么大的样式表就需要88ms。而把它交给网络只是我们烦恼的开始而已:

screenshot

我优化了文件[注2],把它保存到我的机器,用CSSO运行,然后用Gzip的常规设置执行缩小后的内容。看看我得到的数字:

harryroberts in ~/Sites/<client>/review/code on (master)
» csso base64.css base64.min.css

harryroberts in ~/Sites/<client>/review/code on (master)
» gzip -k base64.min.css

harryroberts in ~/Sites/<client>/review/code on (master)
» ls -lh
total 3840
-rw-r--r--  1 harryroberts  staff   925K 10 Feb 11:23 base64.css
-rw-r--r--  1 harryroberts  staff   759K 10 Feb 11:24 base64.min.css
-rw-r--r--  1 harryroberts  staff   232K 10 Feb 11:24 base64.min.css.gz

接下来要做的就是找出有多少字节是Base64资源。为了做这件事,我简单粗暴的删除了所有包含data:字符串(:g/data:/d[注3],Vim用户阅读)的行和声明。这里面大部分是图片/雪碧图,小部分是字体。然后我将这个删除后的文件保存为no-base64.css,再执行相同的压缩和Gzip:

harryroberts in ~/Sites/<client>/review/code on (master)
» ls -lh
total 2648
-rw-r--r--  1 harryroberts  staff   708K 10 Feb 15:54 no-base64.css
-rw-r--r--  1 harryroberts  staff   543K 10 Feb 15:54 no-base64.min.css
-rw-r--r--  1 harryroberts  staff    68K 10 Feb 15:54 no-base64.min.css.gz

在还没有压缩之前,我们已经减少了217K的Base64内容。但还是很大(708K),不过我们已经成功移除了23.45%的Base64代码。

在我们使用Gzip之后,相当惊喜。我们把708K降到了68K。整整减少了90.39%

Gzip保存...

Gzip真的是太让人难以置信了! 它可能是世界上用于保护用户免受开发者祸害的最好工具了。我们只是通过压缩CSS就成功的节省了90%。从708K到68K。

…有时

然而,这是Gzip在没有Base64样式表上的成果。如果我们使用原来的CSS(有Base64),我们只能减少74.91%。

Base64?Gross SizeCompressed SizeSaving
Yes925K232K74.91%
No708K68K90.39%

两个选项之间的差异是惊人的164K(70.68%)。而我们只是通过移开那些更适合其他地方的内容就能够减少164K的CSS

所以,对Base64的压缩是很低效的。下次如果有人说’用Gzip...’,可以给他们看看这些结果(如果他们提倡使用Base64的话)。

为什么Base64这么糟糕?

我们现在已经很清楚在某种程度上Gzip是没办法帮我们处理Base64增加的文件大小,但这只是其中一小部分的问题。为什么我们这么害怕增加文件的大小?单个图片的大小就有可能超过232K,为什么我们不从图片开始解决呢?

好问题,我很高兴你提到图片...

关于图片

为了解释Base64有多糟糕,我们需要先知道图片有多好用。一个颇有争议的观点是:图片的性能没有你想象中的那么差

当然,图片是个问题。实际上它们是页面膨胀的首要贡献者。截止2016年12月2日,图片占了平均网页资源的1623K(64.46%)。对比起来,我们232K的样式表就不足为道了吧。但是,浏览器在处理图片和样式表时是有着本质上的差别的:

图片不阻止渲染,样式会。

不管图片是否加载完浏览器都会开始渲染。即使图片一直都加载不成功,浏览器也会渲染。图片不是关键资源,即使它们占用了过多的字节,它们也不是瓶颈。

而CSS是关键资源。浏览器在构建渲染树之前无法开始渲染页面,在构建CSSOM之前无法构造渲染树,在所有样式表加载完、解压缩和解析之前无法构造CSSOM。CSS才是瓶颈。

现在希望你能够明白为什么我们如此在意CSS的大小:它们只会延迟页面渲染,并且让用户盯着空白的屏幕看。希望你同时能够意识到Base64将图片转换为CSS文件内容是一件很荒谬的事情:为了追求性能,你刚刚将数百K的非阻塞资源变成了阻塞资源。所有这些图片都可以通过网络准备就绪,但它们现在却被迫与关键资源一起出现。这并不意味着图片会更快;它意味着关键资源会更慢。还有比这更糟糕的吗?!

当然有。

浏览器是很聪明的。它为我们做了很多的性能优化,很显然它们更专业。让我们考虑一下响应式:

.masthead {
  background-image: url(masthead-small.jpg);
}

@media screen and (min-width: 45em) {

  .masthead {
    background-image: url(masthead-medium.jpg);
  }

}

@media screen and (min-width: 80em) {

  .masthead {
    background-image: url(masthead-large.jpg);
  }

}

我们给浏览器提供了三种可选的图片,但它只会下载其中一个。它决定需要哪个,然后下载,另外两个则不会使用。

但是如果我们用Base64,三个图片都会下载,实际开销是原本的三倍左右。下面是这个项目一段真实的CSS代码(为了显示,我移除了data,在Gzip之前,这段代码总共是26K;之后是18K):

@media only screen and (-moz-min-device-pixel-ratio: 2),
       only screen and (-o-min-device-pixel-ratio:2/1),
       only screen and (-webkit-min-device-pixel-ratio:2),
       only screen and (min-device-pixel-ratio:2),
       only screen and (min-resolution:2dppx),
       only screen and (min-resolution:192dpi) {

  .social-icons {
    background-image:url("data:image/png;base64,...");
    background-size: 165px 276px;
  }

  .sprite.weather {
    background-image: url("data:image/png;base64,...");
    background-size: 260px 28px;
  }

  .menu-icons {
    background-image: url("data:image/png;base64,...");
    background-size: 200px 276px;
  }

}

所有用户,不论是否使用retina设备(即便用户的浏览器不支持media queries),都将被迫下载额外的18K CSS,然后他们的浏览器甚至会把它们放在一起。

不论是否会被使用,Base64资源一定会被下载。真浪费,但是当你觉得这是浪费的时候,实际上阻碍渲染才是更糟糕的事情。

关于字体

到目前为止,我只提到图片,但是除了浏览器处理无样式/不可见Flash(FOUT或FOIT)的一些细微差别外,字体和图片几乎一样。在这个项目未压缩的CSS中,字体总共有166K(Gzip之后124K,真是可怕的压缩效果)。

不偏离文章主题太远,我们知道字体不是关键资源,这是好事:你的页面渲染不需要它们。但是,不同浏览器会以不同方式处理Web字体:

  • Chrome和Firefox在3s内几乎不显示文字。如果字体在3s内加载成功,文字会从隐藏显示为你的自定义字体。如果字体在3s后还是获取不到,文字就会从隐藏显示为你定义的默认字体。这就是FOIT。

  • IE会立即显示默认字体然后在你的自定义字体加载成功时立即切换。这是FOUT。我个人认为这是最优雅的解决方案。

  • Safari则会一直等待你的自定义字体加载成功才显示文字。如果字体一直获取失败,文字就永远也不会出现。这是FOIT。这真的让人难以接受。你的用户几乎无法看到你网页上的任何文字。

为了解决这些问题,人们使用Base64将字体内联到样式表中:如果CSS和字体同时到达,就不会有什么FOIT或FOUT,因为CSSOM和字体解析几乎会同时发生。

跟图片一样,把你的字体转移到关键资源并不会提高它们的效率,只会延迟你的CSS。实际上有一些非常好的字体加载解决方案,不过Base64不在其中。

再来说说缓存

Base64同时也对我们复杂的缓存机制有影响:通过耦合字体,图片和样式,它们都受同样的规则控制。这意味着即使我们只是随便改变CSS的一个hex值(可能最多就六个字节的数据更改),就需要重新下载几百K的样式,图片和字体。

字体在这里是真正的罪魁祸首:它们是不太可能会改变的稳定资源。事实上,我刚刚检查了另外一个客户和我正在开展的一个长时间运行的项目:他们的CSS昨天刚修改;他们的字体却是在8个月之前修改的。想象一下,每次样式表有什么修改,用户都要被迫重新下载那些不变的字体。

Base64编码意味着我们没办法根据自己的变化来单独缓存内容,也意味着无论是否有改变都需要缓存不变的信息。这是一个不论怎样都输的局面。

我们需要关注基本分离:我的字体缓存不应该依赖于我的图片缓存,我的图片缓存也不应该依赖于我的样式缓存。

好了,让我们快速的概括下:

  • Base64增加了文件的大小但我们却无法有效压缩(e.g.Gzip)。而这种行为会延迟加载,阻塞渲染。

  • Base64把非关键资源(e.g.图片,字体)放到关键资源(e.g.样式表)中。这意味着在这种特殊情况下,在我们开始渲染页面之前,相比起68K的CSS,我们需要下载超过3.4倍的内容。我们白白的让用户等待那些他们原本并不需要等待的内容!

  • Base64强制所有内容都需要下载,即使它们根本就不会被用到。这是一种浪费,而且还发生在我们的关键资源中。

  • Base64限制了我们独立缓存的能力;我们的图片和字体被样式绑定,反之亦然。

总而言之,请避免使用Base64。

用数据说话

这篇文章写的都是我知道的。我并没有执行测试来证明:这只是浏览器的工作原理。但是,我还是决定往前一步,执行一些测试来看看我们所寻求的是怎样的事实和数据。具体请看第二部分内容。


Part2: 数据收集

为了证明使用Base64将静态资源(主要是图片)内嵌到样式表中的缺陷,我决定实际收集一些证据。我设置了一个简单的测试,对比’传统’加载资源和Base64两种方案的一些重要阶段和运行时间。

公平的测试

让我们从两个简单的被背景图片覆盖的HTML文件开始,第一个是普通加载,第二个是用Base64:

  • 普通图片

  • Base64图片

原图是我的朋友Ashley拍摄的。

我把它调整为1440x900px,通过JPEGMini和ImageOptim处理后保存为JPEG,然后才转化为Base64编码:

harryroberts in ~/Sites/csswizardry.net/demos/base64 on (gh-pages)
» base64 -i masthead.jpg -o masthead.txt

这是为了使图片适当优化,得到与实际情况更符合的Base64版本。

接着我新建了两个样式表:

* {
  margin:  0;
  padding: 0;
  box-sizing: border-box;
}

.masthead {
  height: 100vh;
  background-image: url("[masthead.jpg|<data URI>]");
  background-size: cover;
}

我把准备好的演示文件放在了一个实际网址上,这样我们就可以获得真实的延迟和带宽体验。

我在Chrome中打开了一个特定的性能测试配置文件,关闭其他打开的页面,准备好开始。

开启Chrome的Timeline开始测量。整个过程大概是这样:

  • 禁用缓存

  • 清除剩余Timeline信息

  • 刷新页面并记录网络和Timeline活动

  • 丢弃任何关于DNS或TCP的连接结果(我不希望时间受到不相关网络活动的影响)

  • 记录DOMContentLoaded,Load,First Paint,Parse Stylesheet和Image Decode

  • 重复以上步骤直到得到5组干净的数据

  • 隔离每个记录的中位数(中位数是矫正的平均值)

  • 针对Base64再次执行以上所有操作

  • 在移动设备上再做一遍(最终得到四组数据:PC和移动端的Base64和非Base64[注4])

第4点是最重要的:任何连接活动都会使结果发生倾斜并导致不一致,我们只在绝对零连接开销的情况下才保留结果。

测试移动端

我通过调节CPU为3倍,网络为常规的2G,来模拟中档移动设备,并为移动设备完成了大量的测试。

在Google Sheets上你能看到我收集的所有数据(所有数字的单位都是毫秒)。令我震惊的是数据的质量和一致性:很少有异常值。

现在先忽略预加载图片的数据(请看接下来的:第三种方法)。PC和移动端分为不同的表格(切换数据底部的标签)。

一些见解

数据是很直观的,它们证实了我的很多猜想。你自己可以随意查看里面的细节,但我已经提取了最相关和有意义的信息:

  • 在PC和移动端之间,DOMContentLoaded事件在很大程度上保持不变。这里并没有什么’更好的选择’。

  • Load Event在移动端的Base64和非Base64相似,但是PC端的Base64是非Base64的2.02倍(正常:236ms,Base64:476ms)。Base64更慢。

  • parsing stylesheets的Base64明显更慢。在PC端慢了超过10倍。在移动端慢了超过32倍。Base64更慢。

  • 在PC端,Base64解压缩快过普通图片的1.23倍。Base64更快。

  • 但是在移动端,普通图片解压缩快过Base64图片2.05倍。Base64更慢。

  • First Paint是测量感知性能的一个很大的指标:它告诉我们用户何时开始看到内容。在PC端,普通图片的First Paint发生在280ms,但是Base64在629ms:Base64慢了2.25倍

  • 移动端,普通图片的First Paint发生在774ms,Base64在7950ms。Base64慢了10.27倍。换句话说,普通图片在1s内开始绘制,Base64几乎要到8s才开始。令人咋舌,Base64明显比较慢。

通过以上这些信息可以很明显看出谁是完美的赢家:几乎所有方面,所有平台,如果我们远离Base64,会更快。我们尤其需要关注具有更高延迟和受限的性能和带宽的低功耗设备,这里是重灾区:32倍慢的stylesheet和10.27倍慢的First Paint

第三种方法

普通图片的加载有一个问题是对瀑布流的影响:我们需要下载HTML,HTML请求CSS,CSS请求图片,这是同步的过程。Base64的理论优势是可以同时加载CSS和图片(实际上不是,虽然它们一起出现,但是它们也一起迟到了),这可以让我们使用更加并发的方式来加载资源。

幸运的是,有一种方法可以做到不用把所有图片内嵌入样式表就可以实现并行。通过提前加载,而不是将图片作为一个迟来的资源,像这样:

<link rel="preload" href="masthead.jpg" as="image" />

我做了另外一个演示页面:

  • 预加载图片

通过将这个标签放到HTML的头部,我们可以告诉HTML直接下载图片,而不用等CSS去请求它。这意味着,取代像这样的请求链:


                                               |
|-- HTML --|                                   |
           |- CSS -|                           |
                   |---------- IMAGE ----------|
                                               |

我们可以这样:

                                       |
|-- HTML --|                           |
           |---------- IMAGE ----------|
           |- CSS -|                   |
                                       |         
                                       

注意:
我们获取完整内容有多快?
图片在CSS之前加载会怎么样?

预加载可以让我们手动提前加载静态资源,再在之后的页面呈现。

我决定做一个普通图片的页面,取代CSS请求,我打算使用预加载:

<link rel="preload" href="masthead.jpg" as="image" />
<title>Preloaded Image</title>
<link rel="stylesheet" href="image.css" />

我没有发现这个测试用例有多大的改进,预加载在这里并没有很有用:我的请求链太短,我们没有获得重新排序的真正好处。然而,如果我们的页面有许多静态资源,预加载可以给我们带来很大的益处。我在我的主页上使用它来预加载标头:以这种方式使用它确实产生了感知上的一些重大变化。

然而我注意到一个非常有趣的事情,关于解码时间。在移动端,图片在25ms内解码,而PC端需要36.57ms。

  • 预加载图片在移动端解码是PC端的1.46倍快。

  • 预加载图片在移动端解码是没有预加载的3.53倍快。

我不确定为什么会这样,我简单粗暴的猜测:也许图片在实际需要之前不会被解码,所以如果在实际需要解码之前,在设备上已经有一堆字节了,那么这个过程可以更快地工作吗?任何在读这篇文章的人如果有谁知道这个答案的,请告诉我!

改进测试

我虽然尽量保持我的测试公平和不受影响,但如果给我更多时间我可以做得更好(不过这是周末嘛...):

  • 在真实设备上测试。我通过DevTools调节我的CPU和连接,但是在真实设备上运行这些测试无疑会更好。

  • 在移动端用一个更合适的图片。我在尽可能多的变量中保持相同的测试,在PC和移动端使用一样的图片。实际上,我只是模拟移动设备的网络能力,并没有使用较小的屏幕或资源。希望在现实世界中,我们可以为更小的设备提供一张更小的图片(在尺寸和文件大小上)。而我只是在完全相同的视图中加载完全相同的文件,只修改了连接和CPU。

  • 测试一个更真实的项目。这些都是实验,正如我在预加载中指出的,这并没有很有效。希望在非测试环境能够看到不一样的结果。

这就是我关于Base64性能影响的两篇文章。它虽然看起来像是在描述一些我们已经知道的事实,但是能够看到一些数字证明还是很好的,特别是低端设备。Base64感觉上还是像一个巨大的反模式。

[注1] 在一些非常特殊的情况下它也许是明智的选择,但除非你绝对确定,否则那可能就是反模式。总之要非常谨慎,并始终认为Base64不是正确的选择。

[注2] 在Chrome的Sources中打开样式表,按文件左下角的{}。

[注3] 在所有行中运行全局命令(:g); 找到包含数据的行:(/ data :)并删除它们(/ d)。

[注4] 这里需要解释一下:我基本上是在我的笔记本电脑和一个模拟的移动设备上进行测试的,我并不是在说屏幕尺寸。


http://www.niftyadmin.cn/n/1697340.html

相关文章

jQuery Ajax 上传文件改进

如果用户取消上传后 背景 提示自动消失了.... 修正Bug.... 同时也更新了不同上传类型的提示字体大小... 2017-05-26 增加了鼠标释放提示 先看之前的效果&#xff1a; 再看现在的效果&#xff1a; 升级 HTML5文件实现拖拽上传提示效果改进(支持三种状态提示) 拖拽过程详解: 1&am…

python学习总结---函数使用 and 生成器

python学习总结---函数使用 and 生成器 # 函数使用### 生成器- 使用场景在使用列表时&#xff0c;很多时候我们不会一下子使用全部数据&#xff0c;通常都是一个一个使用&#xff0c;但是当数据量比较大的时候&#xff0c;定义一个大的列表将会是内容使用突然增大。为了解决此类…

口令限制参数+口令管理+查看口令限制参数

口令管理 通过配置文件可以实现如下口令管理 1账户锁定 用户连续输入多少次错误口令后&#xff0c;oracle会自动锁定用户的账户&#xff0c;并且规定账户的锁定时间。 2口令的过期时间 用户强制用户定义修改自己的口令&#xff0c;当口令过期后&#xff0c;oracle会自动提…

Kali渗透测试——HexInject

网络数据嗅探工具HexInject 网络数据嗅探是渗透测试工作的重要组成部分。通过嗅探&#xff0c;渗透人员可以了解足够多的内容。极端情况下&#xff0c;只要通过嗅探&#xff0c;就可以完成整个任务&#xff0c;如嗅探到支持网络登录的管理员帐号和密码。 为了实现这个功能&…

mysql-索引-日志

索引&#xff1a;基于元数据之上的在某个字段或多个字段取出来&#xff0c;索引是加速读操作的&#xff0c;但对写操作时有副作用的 BTree 索引&#xff1a;抽取出来重新排序&#xff0c;是左前缀索引每一个叶子结点到根结点的距离相同&#xff1b; 哈希索引&#xff1a;基于键…

(五)SpringBoot如何定义全局异常

一&#xff1a;添加业务类异常 创建ServiceException package com.example.demo.core.ret;import java.io.Serializable;/*** Description: 业务类异常* author * date 2018/4/20 14:30* */ public class ServiceException extends RuntimeException implements Serializable{p…

《Spring MVC学习指南(第2版)》——第1章 Spring框架 1.1XML配置文件

本节书摘来自异步社区《Spring MVC学习指南&#xff08;第2版&#xff09;》一书中的第1章&#xff0c;第1.1节&#xff0c;作者&#xff1a;【美】Paul Deck著&#xff0c;更多章节内容可以访问云栖社区“异步社区”公众号查看 第1章 Spring框架 Spring框架是一个开源的企业应…

Cookie 的运用

Cookie的原理是通过Set-Cookie响应头和Cookie请求头将会话中产生的数据保存在客户端。--- 底层&#xff08;SUN公司已经给我们提供了一套API&#xff09; Cookie是将需要保存的数据保存在了客户端, 是客户端技术. 每个客户端各自保存各自的数据, 再次访问服务器时会带着自己的数…