干货:前端性能优化之图片篇
本文字数:5028字
预计阅读时间:35分钟
在类电商类项目,往往存在大量的图片,如 banner 广告图,菜单导航图,美团等商家列表头图等。图片众多以及图片体积过大往往会影响页面加载速度,造成不良的用户体验,所以对图片进行优化势在必行。
我们先来看一个页面启动时加载的图片信息。
如图所示,这个页面启动时加载了几十张图片(甚至更多),而这些图片请求几乎是并发的,在 Chrome 浏览器,最多支持的并发请求次数是有限的,其他的请求会推入到队列中等待或者停滞不前,直到上轮请求完成后新的请求才会发出。所以相当一部分图片资源请求是需要排队等待时间的,过多的图片必然会影响页面的加载和展示。
选择合适的图片格式
JPEG
JPEG 是由 Joint Photographic Experts Group 所开发出的一种图片。它最大的特点是 有损压缩。这种高效的压缩算法使它成为了一种非常轻巧的图片格式。另一方面,即使被称为“有损”压缩,JPG 的压缩方式仍然是一种高质量的压缩方式:当我们把图片体积压缩至原有体积的 50% 以下时,JPG 仍然可以保持住 60% 的品质。此外,JPG 格式以 24 位存储单个图,可以呈现多达 1600 万种颜色,足以应对大多数场景下对色彩的要求,这一点决定了它压缩前后的质量损耗并不容易被我们人类的肉眼所察觉。
优点
JPEG 格式的图片可以呈现数百万种颜色。所以每当网站需要呈现色彩丰富的图片,JPEG 总是最佳选择。
有损压缩,你可以通过压缩大大的减少图片的体积,一般图片用 60%级别比较合适,如果选择大于 75%的压缩等级,则会使图片有明显的质量下降。
无兼容性问题,所以开发者可以放心随意使用。
使用场景
JPG 适用于呈现色彩丰富的图片,在我们日常开发中,JPEG 图片经常作为大的背景图、轮播图或 Banner 图出现。
但是有损压缩后的图片确实很难露出马脚,当它处理矢量图形和 Logo 等线条感较强、颜色对比强烈的图像时,人为压缩的图片模糊会相当明显。
JPEG 图像不支持透明度处理,透明图片可选择使用 PNG。
PNG
PNG(可移植网络图形格式)是由 W3C 开发的图片格式,是一种无损压缩的高保真的图片格式。它同时支持 8 位和 24 位,这里都是二进制数的位数。按照我们前置知识里提到的对应关系,8 位的 PNG 最多支持 256 种颜色,而 24 位的可以呈现约 1600 万种颜色。
PNG 图片具有比 JPEG 更强的色彩表现力,对线条的处理更加细腻,对透明度有良好的支持。它弥补了上文我们提到的 JPEG 的局限性,唯一的缺点就是 体积太大。
应用场景
PNG 在处理线条和颜色对比度方面的优势,我们主要用它来呈现小的 Logo、颜色简单且对比强烈的图片或背景等。
支持透明度处理,透明图片可选择使用 PNG
GIF
GIF 是一种最多支持 256 种颜色的 8 位无损图片格式。这个限制让 GIF 格式对于多颜色或者摄影图片的展示无能为力。
优点
支持 256 中颜色,文件体积通常都很小
支持透明
应用场景
支持动画,适合去展示一些无限循环的动画,比如图标、表情、广告栏等。
对于一些只有简单色彩的图片非常合适。
WebP
WebP 是一种同时提供了有损压缩与无损压缩(可逆压缩)的图片文件格式,派生自影像编码格式 VP8。它像 JPEG 一样对细节丰富的图片信手拈来,像 PNG 一样支持透明,像 GIF 一样可以显示动态图片,集多种图片文件格式的优点于一身。
WebP 最初在 2010 年发布,目标是减少文件大小,但达到和 JPEG 格式相同的图片质量,希望能够减少图片档在网络上的发送时间。根据 Google 较早的测试,WebP 的无损压缩比网络上找到的 PNG 档少了 45%的文件大小,即使这些 PNG 档在使用 pngcrush 和 PNGOUT 处理过,WebP 还是可以减少 28%的文件大小。
虽然 webP 有诸多优点,但是它不能完全替代 JPEG 和 PNG,因为浏览器对 WebP 支持并不普遍。特别是移动端 IOS 系统基本不支持。
图片压缩
我们再来看一下一张图片的加载过程:
图片众多以及图片体积过大往往会影响页面加载速度,造成不良的用户体验,有部分图片达到几百 kB,甚至 2M(这锅必须运营背,非得上传高清大图不可?),直接导致了加载时间过长。所以对于体积过大的图片,在保持图片在可接受的清晰度范围内可适当对图片大小进行压缩。
图片压缩又分为有损压缩和无损压缩。
有损压缩
有损压缩指在压缩文件大小的过程中,损失了一部分图片的信息,也即降低了图片的质量(即图片被压糊了),并且这种损失是不可逆的。常见的有损压缩手段是按照一定的算法将临近的像素点进行合并。压缩算法不会对图片所有的数据进行编码压缩,而是在压缩的时候,去除了人眼无法识别的图片细节。因此有损压缩可以在同等图片质量的情况下大幅降低图片的体积。例如 jpg 格式的图片使用的就是有损压缩。
无损压缩
无损压缩指的是在压缩图片的过程中,图片的质量没有任何损耗。我们任何时候都可以从无损压缩过的图片中恢复出原来的信息。压缩算法对图片的所有的数据进行编码压缩,能在保证图片的质量的同时降低图片的体积。例如 png、gif 使用的就是无损压缩。
下面是各种图片格式的压缩类型
工具压缩
tinypng 免费、批量、速度块
智图压缩 百度很难搜到官网了,免费、批量、好用
squoosh 在线图片压缩工具
compressor 支持 JPG、PNG、SVG、GIF
webpack 压缩
工程化的项目可以在 webpack 里面配置 image-webpack-loader 进行图片压缩
安装依赖
npm install --save-dev image-webpack-loader
配置 webpack
module.exports = {
...
module: {
rules: [
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[hash:7].[ext]'
},
},
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
progressive: true,
quality: 50,
},
optipng: {
enabled: true,
},
pngquant: {
quality: [0.5, 0.65],
speed: 4,
},
gifsicle: {
interlaced: false,
},
webp: { // 不支持WEBP就不要写这一项
quality: 75
},
},
},
],
},
],
},
}
至于要不要使用插件自动压缩就见仁见智了,因为有些 UI 和产品会说压缩出来的效果图片不是他们想要的。
使用雪碧图
雪碧图,CSS Sprites,国内也叫 CSS 精灵,是一种 CSS 图像合成技术,主要用于小图片显示。
浏览器请求资源的时候,同源域名请求资源的时候有最大并发限制,chrome 为 6 个,就比如你的页面上有 10 个相同 CDN 域名小图片,那么需要发起 10 次请求去拉取,分两次并发。第一次并发请求回来后,发起第二次并发。如果你把 10 个小图片合并为一张大图片的画,那么只用一次请求即可拉取下来 10 个小图片的资源。减少服务器压力,减少并发,减少请求次数。
优点
把诸多小图片合成一张大图,利用 backround-position
属性值来确定图片呈现的位置,可以有效的较少请求个数,而且,而不影响开发体验,使用构建插件可以做到对开发者透明。适用于页面图片多且丰富的场景。
缺点
生成的图片体积较大,减少请求个数同时也增加了图片大小,不合理拆分将不利于并行加载。
合成雪碧图
在 webpack 中,有相应的插件提供了自动合成雪碧图的功能并且可以自动生成对应的样式文件—— webpack-spritesmith,使用方法如下
var path = require('path')
var SpritesmithPlugin = require('webpack-spritesmith')
module.exports = {
// ...
plugins: [
new SpritesmithPlugin({
src: {
cwd: path.resolve(__dirname, 'src/ico'),
glob: '*.png',
},
target: {
image: path.resolve(__dirname, 'src/spritesmith-generated/sprite.png'),
css: path.resolve(__dirname, 'src/spritesmith-generated/sprite.styl'),
},
apiOptions: {
cssImageRef: '~sprite.png',
},
}),
],
}
通过上面配置就能将 src/ico
目录下的所有 png 文件合成雪碧图,并且输出到对应目录,同时还可以生成对应的样式文件,样式文件的语法会根据你配置的样式文件的后缀动态生成。
使用 iconfont
iconfont(字体图标),即通过字体的方式展示图标,多用于渲染图标、简单图形、特殊字体等。
优点
像使用字体一样,设置大小、颜色及其他样式,不失真
轻量,易修改
有效减少 HTTP 请求次数
使用 base64 格式
原理:将图片转换为 base64 编码字符串 inline 到页面或 css 中。
优点
提升性能: 网页上的每一个图片,都是需要消耗一个 http 请求下载而来的, 图片的下载始终都要向服务器发出请求,要是图片的下载不用向服务器发出请求,base64 可以随着 HTML 的下载同时下载到本地.减少 https 请求。
加密: 让用户一眼看不出图片内容 , 只能看到编码。
方便引用: 在多个文件同时使用某些图片时, 可以把图片转为 base64 格式的文件, 把样式放在全局中, 比如 common.css, 以后在用的时候就可以直接加类名, 二不需要多层找文件路径, 会提升效率
但需要注意的是:如果图片较大,图片的色彩层次比较丰富,则不适合使用这种方式,因为该图片经过 base64 编码后的字符串非常大,会明显增大 HTML 页面的大小,从而影响加载速度。
base64 化最常见的就是在 url-loader 中使用。
module.exports = {
...
module: {
rules: [
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10240,
name: utils.assetsPath('img/[name].[hash:7].[ext]'),
}
},
],
},
}
这样就能将项目中小于 10kb 的图片转化为 base64 应用到页面中
使用 css 代替图片。
比如实现修饰效果,如半透明、边框、圆角、阴影、渐变等,在当前主流浏览器中都可以用 CSS 达成,这样能减少图片的请求,达到优化的目的。
缺点
受限于 css 的浏览器的兼容性
对于较复杂的图案就无能为力了,写也麻烦,开发成本大
使用 CDN 图片
CDN 的全称是 Content Delivery Network,即内容分发网络。CDN 是构建在网络之上的内容分发网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN 的关键技术主要有内容存储和分发技术。
举个简单的例子:
❝以前买火车票大家都只能去火车站买,后来我们买火车票就可以在楼下的火车票代售点买了。
基本原理
CDN 的基本原理是广泛采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络中,在用户访问网站时,利用全局负载技术将用户的访问指向距离最近的工作正常的缓存服务器上,由缓存服务器直接响应用户请求。
基本思路
CND 的基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络,CDN 系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。其目的是使用户可就近取得所需内容,解决 Internet 网络拥挤的状况,提高用户访问网站的响应速度。
CDN 的优势
CDN 节点解决了跨运营商和跨地域访问的问题,访问延时大大降低;
大部分请求在 CDN 边缘节点完成,CDN 起到了分流作用,减轻了源站的负载。
图片懒加载
懒加载是一种网页性能优化的方式,它能极大的提升用户体验。图片一直是影响网页性能的主要元凶,现在一张图片超过几兆已经是很经常的事了。如果每次进入页面就请求所有的图片资源,那么可能等图片加载出来用户也早就走了。所以进入页面的时候,只请求可视区域的图片资源。
总结出来就是:
减少资源的加载,页面启动只加载首屏的图片,这样能明显减少了服务器的压力和流量,也能够减小浏览器的负担。 防止并发加载的资源过多而阻塞 js 的加载,影响整个网站的启动,影响用户体验 浪费用户的流量,有些用户并不想全部看完,全部加载会耗费大量流量。
原理
图片懒加载的原理就是暂时不设置图片的 src 属性,而是将图片的 url 隐藏起来,比如先写在 src 里面,等当前图片是否到了可视区域再将图片真实的 url 放进 src 属性里面,从而实现图片的延迟加载。
function lazyload() {
let viewHeight = document.body.clientHeight //获取可视区高度
let imgs = document.querySelectorAll('img[src]')
imgs.forEach((item, index) => {
if (item.dataset.src === '') return
// 用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置
let rect = item.getBoundingClientRect()
if (rect.bottom >= 0 && rect.top < viewHeight) {
item.src = item.dataset.src
item.removeAttribute('src')
}
})
}
// 可以使用节流优化一下
window.addEventListener('scroll', lazyload)
通过上面例子的实现,我们要实现懒加载都需要去监听 scroll 事件,尽管我们可以通过函数节流的方式来阻止高频率的执行函数,但是我们还是需要去计算 scrollTop,offsetHeight 等属性,有没有简单的不需要计算这些属性的方式呢,答案是有的---IntersectionObserver
const imgs = document.querySelectorAll('img[src]')
const config = {
rootMargin: '0px',
threshold: 0,
}
let observer = new IntersectionObserver((entries, self) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
let img = entry.target
let src = img.dataset.src
if (src) {
img.src = src
img.removeAttribute('src')
}
// 解除观察
self.unobserve(entry.target)
}
})
}, config)
imgs.forEach((image) => {
observer.observe(image)
})
图片预加载
图片预加载,是指在一些需要展示大量图片的网站,将图片提前加载到本地缓存中,从而提升用户体验。
常用的方式有两种,一种是隐藏在 css 的 background 的 url 属性里面,一种是通过 javascript 的 Image 对象设置实例对象的 src 属性实现图片的预加载。
1、用 CSS 和 JavaScript 实现预加载
#preload-01 {
background: url(http://domain.tld/image-01.png) no-repeat -9999px -9999px;
}
#preload-02 {
background: url(http://domain.tld/image-02.png) no-repeat -9999px -9999px;
}
#preload-03 {
background: url(http://domain.tld/image-03.png) no-repeat -9999px -9999px;
}
通过 CSS 的 background 属性将图片预加载到屏幕外的背景上。当它们在 web 页面的其他地方被调用时,浏览器就会在渲染过程中使用预加载(缓存)的图片。该方法虽然高效,但仍有改进余地。使用该法加载的图片会同页面的其他内容一起加载,增加了页面的整体加载时间。
为了解决这个问题,我们增加了一些 JavaScript 代码,来推迟预加载的时间,直到页面加载完毕。
function preloader() {
if (document.getElementById) {
document.getElementById('preload-01').style.background =
'url(http://domain.tld/image-01.png) no-repeat -9999px -9999px'
document.getElementById('preload-02').style.background =
'url(http://domain.tld/image-02.png) no-repeat -9999px -9999px'
document.getElementById('preload-03').style.background =
'url(http://domain.tld/image-03.png) no-repeat -9999px -9999px'
}
}
function addLoadEvent(func) {
var oldonload = window.onload
if (typeof window.onload != 'function') {
window.onload = func
} else {
window.onload = function () {
if (oldonload) {
oldonload()
}
func()
}
}
}
addLoadEvent(preloader)
2、使用 JavaScript 实现预加载
function preloader() {
if (document.images) {
var img1 = new Image()
var img2 = new Image()
var img3 = new Image()
img1.src = 'http://domain.tld/path/to/image-001.gif'
img2.src = 'http://domain.tld/path/to/image-002.gif'
img3.src = 'http://domain.tld/path/to/image-003.gif'
}
}
function addLoadEvent(func) {
var oldonload = window.onload
if (typeof window.onload != 'function') {
window.onload = func
} else {
window.onload = function () {
if (oldonload) {
oldonload()
}
func()
}
}
}
addLoadEvent(preloader)
响应式图片加载
什么是响应式图片加载?其实就是在不同分辨率的设备上显示不同尺寸的图片,避免资源的浪费。
常用的方法就是 css3 的媒体查询(media query)。
@media screen and (min-width: 1200px) {
img {
background-image: url('1.png');
}
}
@media screen and (min-width: 992px) {
img {
background-image: url('2.png');
}
}
@media screen and (min-width: 768px) {
img {
background-image: url('3.png');
}
}
@media screen and (min-width: 480px) {
img {
background-image: url('4.png');
}
}
此外,还可以使用 HTML5 的 picture 属性进行响应式处理。方法如下:
创建 picture 标签。
放置多个 source 标签,以指定不同的图像文件名,进而根据不同的条件进行加载。
添加一个回退的元素
<picture>
<source srcset="src/img/l.png" media="(min-width: 1200px)" />
<source srcset="src/img/2.png" media="(min-width: 992px)" />
<source srcset="src/img/4.png" media="(min-width: 768px)" />
<img src="src/img/4.png" />
picture>
需要注意的是:现在很多浏览器对于 picture 这个标签还不支持,使用的时候需要加以注意。
渐进式图片
渐进式图片的意思是在高画质图像加载完之前会先显示低画质版本。低画质版本由于画质低、压缩率高,尺寸很小,加载很快。在两者之间我们也可以根据需要显示不同画质的版本。
渐进式图片可以让用户产生图片加载变快的印象。用户不再盯着一片空白区域等待图片加载,而能看到图像变得越来越清晰,这样对用户体验也是友好的。
骨架屏技术也是类似的原理。
总结
选择合适的图片格式和压缩大图,可从根源上截图大图加载过慢的问题。
使用雪碧图,iconfont,base64,css 代替图片等可减少图片 http 请求,提高页面加载速度。
使用 CDN 图片可达到分流的效果,减少服务券压力。
图片懒加载,预加载,渐进式图片等可不同程度减少白屏时间,提高产品体验。