一年一度的程序员节来啦,当然大家期待的 10·24征文活动也随之而来喽,现在来分享一下我在公司性能优化落地的一些实践,希望能帮助大家。
一、前言:
从刚毕业到现在也有近4个年头,最开始接触的是PHP + jQuery的开发,到后面的SOA分布式、K8s微服务,目最新团队使用了一段时间AI辅助开发,接触的业务规模也是越来越大,在不断的项目驱动学习与自我技能提高中,在应对越来越复杂的项目,做了很多全链路的优化。
阶段一(全栈开发工程师):
就职于一家外包公司,公司发展的初级阶段,人比较少,对产品迭代速度的要求较高,此时更多的需要一些全栈的工程师,一个人开发从前到后全搞定,主要是做一些CMS、写一些官网、一些jQuery H5的页面,Laravel Blade模板+ Bootstrap的后台管理系统模板,所用的技术相对比较落后,而且每天都是比较重复工作,外包的业务量较小,也不需要考虑并发、事务场景,往往索引都不需要加。如下为经常使用的Bootstrap的后台管理系统模板,基本上靠开发“3板斧头闯天下”。
阶段二(开发组长 -> 技术经理):
进入一家初创企业公司,公司人员配置还是比较正规的,前后端分离的工作模式,有专业的运维岗位,相对于全栈开发,虽然增加了些沟通成本比,但是对于产品迭代的质量还是有所保证的,从某些角度来看,其实还是提高了效率,前后端分离后可以使前后端工程师分工更加明确,大家各司其职,提高工作效率,充分发挥各自的长处。
- 让后端工程师专注于业务逻辑的实现以及性能优化、安全。
- 前端工程师专注于用户体验,交互模式。
公司也是逐渐规模越来越大,也遇到了很多问题,现在就跟大家一起来分享一下,在企业内部落地的一些优化方案的最佳实践。性能优化的关键在于从多个角度和层面进行综合优化,包括但不限于代码优化、数据库优化、服务器优化、前端优化等。
二、全栈工程化体系 – 最佳实践落地优化方案:
三、【场景一】:前端工程化体系一:Webpack打包方案优化:
1. 问题点描述:
项目使用是Vue开发的后台管理系统,随着项目逐渐增加新功能,没有做一定的优化策略,项目打包越来越慢了,而且网站的首屏白屏加载时间也越来越多,用户体验越来越差了,同时,也遭到业务的各个部门的投诉,技术团队内部紧急解决这个问题。
2. 原因分析:
打包的文件太多,有序号的文件就将近733个chunk文件,总的大小也快接近50Mb的大小,这样会导致文件加载越来越慢,而且每次发版本的时候,都需要重新刷新一下整个hash值的文件。
3. 解决思路分析:
4. 技术点思路分析:
可视化打包体积大小分析插件:webpack-bundle-analyzer。
(1). 作用:
查看打包后的文件大小与其中组成成分,可以查看哪些文件占用较大。
- ①. 打包后所有组件与组件间的依赖关。
- ②. 针对多余的包文件过大,对首次影响加载的效率问题进行剔除修改。
- ③. 将捆绑内容表示为方便的交互式可缩放树形图【可视化试图】。
(2). 模块功能:
- ①. 文件打包压缩后中真正的内容。
- ②. 找出哪些模块组成最大的大小。
- ③. 找到错误的模块。
- ④. 支持缩小捆绑,会解析它们实际大小的捆绑模块、gzipped大小。
(3). 实现:
首先进行“webpack-bundle-analyzer”这个插件的安装,使用npm install和yarn install都可以,看自己的需求进行安装,可以根据自己的webpack版本来决定安装的版本([email protected])。
yarn add webpack-bundle-analyzer@ 4.10.2
修改“webpack.prod.config.js”文件中,增加引入“webpack-bundle-analyzer”这个插件,在Plugins插件属性中,增加“new BundleAnalyzerPlugin()”实例化的对象,可以使用默认的配置即可。
const webpack = require('webpack');
...
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = merge(webpackBaseConfig, {
output: {
publicPath: '/dist/',
filename: '[name].[hash].js',
chunkFilename: '[name].[hash].chunk.js'
},
plugins: [
new BundleAnalyzerPlugin(), // 使用默认配置
......
]
});
不过,需要配合 webpack 和 webpack-cli 一起使用,运行“yarn build”命令后,打包完成会自动在在网页上显示一个report.html的文件项目资源包的交互式可视化树形分析图,可以直观地分析打包出的文件有哪些,及它们的大小、占比情况、各文件 Gzipped 后的大小、模块包含关系、依赖项等,对应做出优化,从而帮助提升代码质量和网站性能。
可以看到上面的可视化树形分析图,有些Chunk文件很大,占面积越大的文件,其文件的大小越大,最大的达到了4.22MB,部署到服务器之后,在浏览器中加载的时间越长,鼠标移动到相对应的板块上,就可以直接查看各个文件的大小,得到可视化的分析结果之后,接下来就对占比比较大或者依赖程度比较高的文件进行优化了。
通过上述的分析图可以帮助我们得到一些项目存在的问题点信息:
- ①. 了解Chunk包中的真正内容,以及打包后生成了哪些文件
- ②. 找出哪些Chunk包模块尺寸最大,哪些文件会存在复重(如图中Tinymce富文本),看起来是每个文件夹都有一套代码,没有公用起来
- ③. 查找误引入的模块,比如说有些模块根据在项目中没有使用到,或者使用过了,在后续的迭代项目中更换了功能,但是代码还在。
- ④. 通过“Gzipped”选项,可以查看Chunk包文件压缩之后的大小,并且与原始文件实际大小进行比较,压缩率是多少。
当然,此时,在生产流水线打包,肯定会报错的,可以作为CLI的一个工具来使用,直接可以打包为一个json文件用于分析,可以在package.json中scripts命令中增加一条命令:
"build:chart": "vue-cli-service build --profile --json > stats.json",
详细的参数介绍:
new BundleAnalyzerPlugin({
// 可接受字符串,用来指定报告生成的格式和渠道。可以是'server','static',或者'disabled'。
// 'server'会启动一个HTTP服务器来提供报告的互动查看,默认为localhost:8888。
// 'static'会生成单个HTML文件来描述报告。
// 'disabled'在有些场景中可能很有用,你可以用于临时禁用插件,但保留配置代码。
analyzerMode: 'server',
// 在'server'模式下,定义HTTP服务器的主机名。
analyzerHost: '127.0.0.1',
// 在'server'模式下,定义HTTP服务器的端口号。
analyzerPort: 8888,
// 在'static'模式下,定义报告生成的文件名。
reportFilename: 'report.html',
// 定义模块大小的计算方式。定义为'stat','parsed'或者'gzip'。
// 'stat'描述所有未解析代码的大小之前的大小(模块间关联的尺寸)。
// 'parsed'描述所有已被解析的代码的大小(只有已经经过loaders处理和解析的代码)。
// 'gzip'则会描述所有解析代码的gzip大小(如果这个值被定义的话)。
defaultSizes: 'parsed',
// 一个模块是否应该被展示出来。你可以通过这个选项来隐藏那些非必要的模块。
// 你可以传入一个函数,这个函数接收一个模块作为参数,然后返回一个布尔值。
filterModules: false,
// 如果你想要生成的报告文件的内容是一个自定义的数据结构,你可以传入一个函数,这个函数会接收一个stats数据作为参数。
// 你的函数需要返回一个序列化的JSON对象,或者一个Promise,这个Promise在resolve时需要返回这个序列化的JSON对象。
generateStatsFile: false,
// 如果你想要自定义生成的stats文件的文件名,你可以修改这个选项。
statsFilename: 'stats.json',
// 定义在生成stats文件时,是否也生成报告。
statsOptions: null,
// 定义一个日志函数。
logLevel: 'info'
})
5. 落地方案实施一(Gzip压缩):
Gzip是一种 http 请求优化方式,一种用来改进web应用程序性能的技术,gzip压缩比率在3到10倍左右,可以大大节省服务器的网络带宽。
- ①. 通过减少文件体积来提高加载速度。
- ②. 对于用户量多的网站,开启 gizp 压缩会大大降低服务器压力,提高加载速度、降低服务器流量成本。
- ③. 节省了服务器的网络带宽,节约的流量非常可观。
- ④. 必须浏览器与服务器都支持Gzip。
- ⑤. Gzip算法特性,代码相似率越大压缩效率越高(风浪越大,鱼越贵)。
如下左图描述了Gzip的工作原理图:
-
①. 浏览器发送请求:
- a. 在 request header 中设置属性 accept-encoding:gzip。
- b. 表示浏览器支持 gzip。
-
②. 服务器收到请求后:
- a. 判断浏览器是否支持 gzip:
- (1). 如果支持 gzip,则向浏览器传送压缩过的内容。
- (2). 不支持则向浏览器发送未经压缩的内容。
- b. Response headers返回包含 content-encoding:gzip。
- a. 判断浏览器是否支持 gzip:
-
③. 浏览器接收响应后判断内容是否被压缩,如果被压缩则解压缩显示页面内容,浏览器先解压再使用,对于用户是无感知的。
从右侧的可视化树形分析图中可以看到,原始的文件大小是47.74MB,通过Gzipped压缩之后的代码是5.75MB,相当于节省了88%的空间大小,可以大大的加速了远程资源的加载,同时也节省了带宽,所以说导入Gzip压缩是快速体现加载速度最快捷,收益也是最大的方式。
两种 Gzip 实现的压缩方式:
-
①. webpack打包生成 .gz 文件【推荐】:
- a. 通过 webpack 配置生成对应的 .gz 文件。
- b. 浏览器请求 xx.js/css 等文件时,服务器返回对应的 xxx.js.gz 文件。
-
②. 服务器实时在线将请求 xx.js 文件进行gzip压缩后传输给浏览器:
- a. 压缩文件过程本身有额外开销。
- b. 服务器压缩的时间开销和 CPU 开销(及浏览器解析压缩文件的开销)为代价【重点】,来节省传输过程中的时间开销。
方案一:Nginx服务器Gzip压缩方案:
server {
...
gzip on;
gzip_buffers 4 16K;
gzip_comp_level 6;
gzip_min_length 100k;
gzip_types text/plain application/x-javascript application/javascript application/json text/css application/xml text/javascript application/x-httpd-php;
gzip_disable "MSIE [1-6].";
gzip_http_version 1.1;
gzip_vary on;
...
}
参数解释:
// 是否开启gzip
gzip on|off;
// 压缩的页面最小字节数,超出进行压缩
// 页面字节数从header头中的Content-Length中进行获取,默认值是0.
// 太小就不要压缩了,意义不大
gzip_min_length 100k;
// 缓冲区(压缩在内存中缓冲几块?每块多大?)
// 获取多少内存用于缓存压缩结果:'4 16k'表示以16k*4为单位获取
gzip_buffers 4 16k;
// 压缩级别(压缩比1-9,级别越高,压的越小,越浪费CPU计算资源)
// 越小压缩效果越差,但是越大处理越慢,一般取中间值(官网建议6)
gzip_comp_level 6;
// 对哪些类型的文件用压缩,如txt、xml、html、css、js等
// 对特定的MIME类型生效,其中'text/html'被系统强制启用
gzip_types text/plain;
// 识别http协议的版本,不支持gzip自解压的浏览器,用户会看到乱码
gzip_http_version 1.1
// 是否传输gzip压缩标志
// 和http头有关系,加个vary头,给代理服务器用的,有的浏览器支持压缩,有的不支持
// 避免不支持的也压缩,根据客户端的HTTP头来判断,是否需要压缩
gzip_vary on|off;
// nginx做为反向代理时启用,off(关闭所有代理结果的数据的压缩),expired(启用压缩,如果header头中包括"Expires"头信息),no-cache(启用压缩,header头中包含"Cache-Control:no-cache"),no-store(启用压缩,header头中包含"Cache-Control:no-store"),private(启用压缩,header头中包含"Cache-Control:private"),no_last_modefied(启用压缩,header头中不包含"Last-Modified"),no_etag(启用压缩,如果header头中不包含"Etag"头信息),auth(启用压缩,如果header头中包含"Authorization"头信息)
gzip_proxied off
// IE5.5和IE6 SP1使用msie6参数来禁止gzip压缩
// 指定哪些不需要gzip压缩的浏览器(将和User-Agents进行匹配),依赖于PCRE库
gzip_disable "MSIE [1-6]."
注意事项:
-
①. 图片/mp3的二进制文件:
- a. 不必压缩,因为压缩率比较小,同时会耗费CPU资源的。
- b. 图片压缩并不能实际减少文件大小,反而会导致打包后生成很多同大小的gz文件。
-
②. 比较小的文件不必压缩。
方案二:Webpack前端Gzip压缩方案【推荐】:
Webpack前端Gzip压缩插件:compression-webpack-plugin
安装 compression-webpack-plugin插件:
yarn add compression-webpack-plugin@6.1.1 -D
注:新版本 7.x 会报错,Cannot read property ‘tapPromise’ of undefined
在 vue.config.js 中配置:
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
chainWebpack(config) {
...
// 方式一【本文使用】:
config
.when(process.env.NODE_ENV === 'production',
config => {
config
.plugin('compression')
.use(CompressionPlugin)
.tap(() => [{
test: /\.js$|\.html$|\.css$/, // 匹配文件名,开启js、css压缩
filename: '[path].gz[query]', // 压缩后的文件名(保持原文件名,后缀加.gz)
minRatio: 1, // 压缩率小于1才会压缩
threshold: 10240, // 对超过10k的数据压缩
deleteOriginalAssets: false // 是否删除未压缩的源文件(不设置或设置为false)
// 保留非gzip的资源,删除打包后的gz后还可以加载到原始资源文件,建议不要设置为true
}])
}
)
// 方式二:
if (process.env.NODE_ENV === 'production') {
config.plugin('compression-webpack-plugin')
.use(new CompressionPlugin({
test: /\.js$|\.html$|\.css/,
threshold: 10240,
deleteOriginalAssets: false
}))
}
}
}
使用“yarn build”打包后目录会多出 .gz 文件,需要注意的如果没有设置filename的话,打包的目录会只有一个没有名称的 .gz 文件,并提示“Conflict: Multiple assets emit different content to the same filename static/js/.gz”。
当然,Nginx服务器也是需要开启 Gzip的功能,“gzip_static”参数开启后,Nginx就会读取预先压缩的gz文件,可以减少每次请求进行Gzip压缩的CPU资源消耗。
server {
// 表示静态加载本地的gz文件
// 浏览器请求xx.js/css等文件时,服务器返回对应的xxx.js.gz文件
// 服务器会根据Request Headers的Accept-Encoding标签进行鉴别,如果支持gzip就返回.gz文件.
// gzip_static开启后,nginx就会读取预先压缩的gz文件,可以减少每次请求进行gzip压缩的CPU资源消耗
gzip_static on;
gzip_http_version 1.1;
}
这是我们在其它项目上的应用截图,可以看到加载速度还是有所提高,这个项目是3.2MB,可能性价比看不出来太高,但是如果50MB、100MB的话,性价比的优势非常明显。
可以通过CURL的命令来,检查是否开启Gzip成功,如果content-encoding返回的值是“gzip”就可以证明已经是被Gzip压缩过了。
curl -I -H "accept-encoding: gzip, deflate" "https://xxxx/static/css/chunk-elementUI.a8b08852.css"
也可以通过F5,查看Network的情况,通过勾选“Big request rows”选项,来查看未压缩的资源大小的比对,如果发现两个大小不一样,同样,也可以表示Gzip压缩过。
以下是增加Gzip 压缩的一个实际效果对比的结果,我们可以找了其中的一个js文件“chunk-elementUI.71006ee3.js”来做一下比较,看看Gzip 压缩前后有没有什么大的变化:
- ①. 压缩前:在压缩之前这个js文件的加载时间是8.31s,文件的传输大小为593kB。
- ②. 压缩后: 在压缩之前这个js文件的加载时间是1.75s,文件的传输大小为146kB。
- ③. Gzip的压缩比率在4倍左右。
可以通过Url的请求来对Request、Response 进行比对:
-
①. Request Headers:
- a. Accept-Encoding: gzip, deflate:
- (1). 表示用户浏览器支持二种压缩,包括 gzip 的压缩方式。
- (2). deflate 与 gzip 使用的压缩算法几乎相同。
- a. Accept-Encoding: gzip, deflate:
-
②. Response Headers:
- a. Accept-Ranges: bytes:
- (1). 是用来告诉客户端,服务器是否能处理范围请求,以指定获取服务器端某个部分的资源。
- (2). 表示能处理客户端发过来的范围请求。
- b. 静态加载 gz 文件的响应头:Content-Encoding: gzip
- a. Accept-Ranges: bytes:
注意事项:
- ①. Nginx服务器配置了静态 gz 加载后,请求文件变小不会导致请求卡线程。
- ②. 保留了源文件,当删除 gz 后,浏览器会自动去请求原始文件,不会导致界面出现任何问题。
四、【场景二】:前端工程化体系二:图片Webp导入方案优化:
1. 问题点描述:
项目在早期使用的过程中,存在大量的png和jpeg类型图片,类似H5的下单页面会有大量的图片显示,比如商品的描述,商品的介绍、商品的规则参数、商品的头图等等,而首页是一个门户,如果打开经常延迟、卡顿,白屏时间过长,容易导致用户不好的体验,就不会来购买下单,可以看一下我们早期网站加载的问题点:
- ①. 加载的图片最大的大小是1.4MB,200KB以上的图片占60%。
- ②. 加载的图片最长时间是9.93s,超过3s钟加载的占40%。
- ③. 总体首页加载的资源是12.7MB,加载完成的时间为12s。
2. 原因分析:
在当今互联网数字化的时代,图片在网页设计和内容传递中扮演着至关重要的角色。为了提供更好的用户体验和更佳的网页性能,互联网图片格式从最早期的 BMP、PCX全面升级到JPG、PNG 等主流格式,而 WebP 作为一种新兴的图像格式,正以其众多优势在行业中崭露头角。
目前对于JPEG、PNG、GIF等常用图片格式的优化已几乎达到极致,因此Google于2010年提出了一种新的图片压缩格式 — WebP,WebP为网络图片提供了无损和有损压缩能力,使用无损压缩后的WebP比PNG文件少了26%的体积,有损压缩后的WebP图片相比于等效质量指标的JPEG图片减少了25%~34%的体积。
3. 解决思路分析:
WebP最初在2010年发布,目标是减少文件大小,但达到和JPEG格式相同的图片质量,希望能够减少图片档在网络上的发送时间。2011年11月8日,Google开始让WebP支持无损压缩和透明色(alpha通道)的功能,而在2012年8月16日的参考实做libwebp 0.2.0中正式支持。
WebP 是一种由 Google 开发的现代图像格式,旨在提供高质量的图像压缩,同时保持较小的文件大小,WebP 支持有损和无损压缩,以及透明度(alpha 通道),并且它特别适合用于网络上的图像传输,因为它可以帮助减少带宽消耗和加快页面加载速度。
虽然 WebP 具有这些优势,但由于一些老旧的浏览器可能不支持该格式,因此在使用 WebP 时需要确保目标受众的浏览器兼容性。此外,对于不支持 WebP 的浏览器,可以提供回退方案,如使用 JPEG 或 PNG 格式的图像。
4. 技术点思路分析及落地实践:
以下在蓝湖上进行切图时,默认一般是使用png或者jpg格式,现在可以增加webp格式,点击下载后,可以同时放到文件夹下面。
可以通过Picture标签可以通过包含一个或多个元素来使用,根据屏幕匹配的不同尺寸显示不同图片,如果没有匹配到或浏览器不支持 picture 属性则使用 img 元素,用法如下,标签包含了三个元素:
<picture>
<source srcset="~images/index/product.webp" type="image/webp">
<source srcset="~images/index/product.png" type="image/png">
<img src="~images/index/product.jpg" alt="Picture">
</picture>
- ①. 多个图片源:可以指定多个图片源,根据浏览器的支持情况来选择最合适的图片格式,在上面例子中,首先尝试加载product.webp,如果浏览器不支持webp格式,则加载product.png,最后再加载product.jpg。
- ②. 标签的默认内容:它会在没有匹配的标签时被加载。
- ③. src属性:用于指定默认情况下要加载的图片地址。
- ④. alt属性:用于指定图片的替代文本,当图片无法加载时会显示这个文本。
- ⑤.
使用Picture标签可以提供更好的图片适配性,根据不同设备和浏览器的支持情况加载不同的图片格式,从而提高页面的性能和用户体验。
以上通过Picture标签方案,在H5的项目的落地实施,可以看到空间压缩率平均可以减少达到72%左右,加载的时间平均可以降低70%左右,通过比较发现:
- 加载体积最大的图片1.4MB,转换为webp格式图片大小为526kb。
- 加载耗时最大的图片9.03s,转换为webp格式图片时间为1.41s。
五、【场景三】:前端工程化体系三:图片压缩方案优化:
在日常的Web开发或设计工作中,经常需要处理大量的图像文件,图片大小直接影响页面加载速度,进而影响用户体验。
1. 问题点描述:
承上启下,页面的图片越大那么网站的打开速度也就会越慢,所以在保证图片的清晰度的同时我们也要适当的压缩图片的大小来保证网站的打开速度,提高网站的质量得分。
根据Google较早的测试,WebP的无损压缩比网络上找到的PNG档少了45%的文件大小,即使这些PNG档在使用pngcrush和PNGOUT处理过,WebP还是可以减少28%的文件大小。
2. 技术点思路分析:
TinyPng 使用了先进的压缩算法,对PNG和JPEG图片进行智能优化,其主要采用了以下技术:
TinyPng可广泛应用于以下几个场景:
- ①. Web开发:优化网站上的图片资源,加快页面加载速度。
- ②. 移动应用:减少安装包大小,提高用户下载与更新的效率。
- ③. 图形设计:在不影响视觉效果的前提下,减小设计作品的存储需求。
- ④. 云服务:与图像处理服务结合,提升服务质量。
3. 落地实践【方案一】在线网站压缩:
在项目开发的前期,维护同学是在TinyPng在线的网站上进行上传图片,压缩完成后,再下载替换到项目中,可以看到png、webp、jpeg等主要使用的图片格式都能进行压缩:
以某一张图片来讲,最开始的png大小为1.2MB,转为webp格式图片大小为737KB,再进行webp图片压缩,可以达到66KB。
不过,这样做的话,有一个缺点,就是每次有图片更新话,都需要人工手动去进行上传压缩,想想该如何进行自动化压缩图片呢?
4. 落地实践【方案二】开发压缩CLI小工具:
上面也提到了TinyPng是可以进行API访问的,基于API的形式,开发了一个CLI的小工具,在webpack构建之前,会先去运行这个CLI的命令来进行图片的文件压缩,实现自动化进行压缩图片,如下CLI工具会把一些主要的信息在控制台命令中进行显示:
- ①. Webp在压缩后,可以节省近70%的空间大小
- ②. Webp在压缩后,可以加速近33%的时间大小
以下为CLI相关的主要代码:
#!/usr/bin/env node
const inquirer = require('inquirer')
const ora = require('ora')
const cluster = require('cluster')
import chalk from 'chalk'
import fs from 'fs'
import Ora from 'ora'
import {
resolve } from 'path'
let spinner: Ora.Ora // ora载体
let inputSize = 0 // 输入总体积
let outputSize = 0 // 输出总体积
let ratio = 0 // 压缩比
/**
* 程序入口
*/
interface importData {
name?: string
}
const test = (): void => {
inquirer
.prompt([
{
type: 'input',
message: '[cx-tinypng] 请输入当前目录下需要压缩的 (文件夹名称)?',
name: 'name'
}
])
.then((res: importData) => {
const folderName = res.name
const spinner = ora(`正在检查文件夹[${
folderName}]是否存在......`)
spinner.start()
console.log(res)
// 找出所有目标文件夹
const targetFolder = resolve(process.cwd() + "/" +folderName)
const isTargetFolder = fs.existsSync(targetFolder);
if (isTargetFolder) {
mapFolder(targetFolder)
} else {
console.log("目录不存在")
}
spinner.stop()
})
}
test()
/**
* 递归找出所有图片
* @param { string } path
* @returns { Array<imageType> }
*/
export type imageType = {
path: string
file: Buffer
}
interface IdeepFindImg {
(path: string): Array<imageType>
}
let deepFindImg: IdeepFindImg = (path: string) => {
const content = fs.readdirSync(path)
let images: Array<imageType> = []
content.forEach(folder => {
const filePath = resolve(path, folder)
const info = fs.statSync(filePath)
if (info.isDirectory()) {
images = [...images, ...deepFindImg(filePath)]
} else {
const fileNameReg = /\.(jpe?g|png|svga)$/
const shouldFormat = fileNameReg.test(filePath)
if (shouldFormat) {
const imgData = fs.readFileSync(filePath)
if (filePath && imgData) {
images.push({
path: filePath,
file: imgData
})
}
}
}
})
return images
}
/**
* 遍历处理每个目标文件
* @param { Array<string> } folderList
*/
interface ImapFolder {
(folderPath: string): void
}
const mapFolder: ImapFolder = async (folderPath: string) => {
// 查找目标文件夹内的图片资源
const targets = deepFindImg(folderPath)
folderList.forEach(path => {
target = [...target, ...deepFindImg(path)]
})
if (target.length) {
const noCompressList: Array<imageType> = [] // 未压缩列表
const hasCompressList: Array<imageType> = [] // 已压缩列表
let len = 0
while (len < target.length) {
const {
path } = target[len]
let data = ''
const curBuf: Buffer = await new Promise((resolve, reject) => {
const readerStream = fs.createReadStream(path)
readerStream.setEncoding('utf8')
readerStream.on('data', chunk => {
data += chunk
})
readerStream.on('end', () => {
const buf = Buffer.alloc(data.length, data, 'binary')
resolve(
Buffer.from(
toArrayBuffer(buf).slice(buf.length - tagLen, buf.length)
)
)
})
readerStream.on('error', err => {
reject(err.stack)
})
})
try {
if (curBuf.compare(tagBuf) !== 0) {
noCompressList.push(target[len])
} else {
hasCompressList.push(target[len])
}
} catch (err) {
spinner.fail(`读取 ${
path} 资源失败!`)
}
len++
}
// 未压缩的svga数量
const noCompressSvgaNum = noCompressList.filter(ele =>
/\.(svga)$/.test(ele.path)
).length
// 未压缩的图片数量
const noCompressImageNum = noCompressList.length - noCompressSvgaNum
// 已压缩的svga数量
const hasCompressSvgaNum = hasCompressList.filter(ele =>
/\.(svga)$/.test(ele.path)
).length
// 已压缩的图片数量
const hasCompressImageNum = hasCompressList.length - hasCompressSvgaNum
CG({
options: {
headerVisible: true
},
columns: ['类型', '可压缩', '已压缩', '总数'],
rows: [
[
'图片',
chalk.red(noCompressImageNum),
chalk.green(hasCompressImageNum),
chalk.blue(noCompressImageNum + hasCompressImageNum)
],
[
'SVGA',
chalk.red(noCompressSvgaNum),
chalk.green(hasCompressSvgaNum),
chalk.blue(noCompressSvgaNum + hasCompressSvgaNum)
]
]
})
if (!noCompressList.length) {
spinner.fail(`「目标文件夹内」找不到「可压缩」的资源!`)
spinner.stop()
return
}
inquirer
.prompt([
{
type: 'list',
message: chalk.green('[yx-tiny]') + ' 请选择压缩模式?',
name: 'compressType',
choices: [
{
value: 'all',
name: '全 量'
},
{
value: 'diy',
name: '自定义'
}
],
pageSize: 2
},
{
type: 'checkbox',
message: chalk.green('[yx-tiny]') + ' 请选择需要压缩的图片?',
name: 'compressList',
choices: target.map(img => ({
value: img, name: img.path })),
pageSize: 10,
when: function ({
compressType }: {
compressType: 'diy' | 'all' }) {
return compressType === 'diy'
}
}
])
.then(
async ({
compressType,
compressList
}: {
compressType: 'diy' | 'all'
compressList: Array<imageType>
}) => {
// 根据用户选择处理对应的资源
const list = compressType == 'all' ? noCompressList : compressList
if (!list.length) {
spinner.fail(`请至少选择一个!`)
spinner.stop()
return
}
// 开始时间
const dateStart = +new Date()
cluster.setupPrimary({
exec: resolve(__dirname, 'features/process.js')
})
// 若资源数小于则创建一个进程,否则创建多个进程
const works: Array<{
work: any; tasks: Array<imageType> }> = []
if (list.length <= cpuNums) {
works.push({
work: cluster.fork(), tasks: list })
} else {
for (let i = 0; i < cpuNums; ++i) {
const work = cluster.fork()
works.push({
work, tasks: [] })
}
}
// 平均分配任务
let workNum = 0
list.forEach(task => {
if (works.length === 1) {
return
} else if (workNum >= works.length) {
works[0].tasks.push(task)
workNum = 1
} else {
works[workNum].tasks.push(task)
workNum += 1
}
})
// 用于记录进程完成数
let pageNum = works.length
let succeedNum = 0 // 成功资源数
let failNum = 0 // 失败资源数
const failMsg: Array<string> = [] // 失败列表
let outputTabel: Idetail[] = []
// 初始化进度条
bar.render({
current: 0,
total: list.length,
token: `${
chalk.green(0)} 个成功 ${
chalk.red(0)} 个失败`
})
works.forEach(({
work, tasks }) => {
// 发送任务到每个进程
work.send(tasks)
// 接收任务完成
work.on('message', (details: Idetail[]) => {
outputTabel = outputTabel.concat(details)
// 统计 成功/失败 个数
details.forEach((item: Idetail) => {
if (item.output) {
inputSize += item.input
outputSize += item.output
ratio += item.ratio
succeedNum++
} else {
failNum++
if (item.msg) failMsg.push(item.msg)
}
// 更新进度条
bar.render({
current: succeedNum + failNum,
total: list.length,
token: `${
chalk.green(succeedNum)} 个成功 ${
chalk.red(
failNum
)} 个失败`
})
})
pageNum--
// 所有任务执行完毕
if (pageNum === 0) {
if (failMsg.length) {
failMsg.forEach(msg => {
spinner.fail(msg)
})
}
CG({
options: {
headerVisible: true
},
columns: [
'名称',
'原体积',
'现体积',
'压缩率',
'耗时',
'状态'
],
rows: [
...outputTabel.map((item: any) => [
chalk.blue(filterFileName(item.path)),
chalk.red(byteSize(item.input)),
chalk.green(byteSize(item.output)),
!item.ratio
? chalk.red('0 %')
: chalk.green((item.ratio * 100).toFixed(4) + ' %'),
chalk.cyan(item.time + ' ms'),
item.output ? chalk.green('success') : chalk.red('fail')
])
]
})
const totalRatio = ratio / succeedNum
spinner.succeed(
`资源压缩完成! \n原体积: ${
chalk.red(
byteSize(inputSize)
)}\n现体积: ${
chalk.green(byteSize(outputSize))}\n压缩率: ${
totalRatio
? chalk.green((totalRatio * 100).toFixed(4) + ' %')
: chalk.red('0 %')
}\n成功率: ${
chalk.green(
((succeedNum / list.length) * 100).toFixed(2) + ' %'
)}\n进程数: ${
chalk.blue(works.length)}\n总耗时: ${
chalk.cyan(
+new Date() - dateStart + ' ms'
)}\n`
)
cluster.disconnect()
}
})
})
}
)
} else {
spinner.fail(`找不到可压缩资源!`)
spinner.stop()
}
}
六、【场景四】:前端工程化体系四:上传图片压缩方案优化:
1. 问题点描述:
很多场景中需要拍照上传图片,比如业务有用到案件,需要拍高清的图片上传用来审核,但是高分辨率的照片文件在存储和传输过程中占用较多的空间,并且会增加上传和下载的时间成本。在以下情况下,对照片进行大小压缩尤为重要:
通过对照片进行大小压缩,可以有效地节省存储空间并提高上传速度,如果我们直接将原图片上传,可以图片体积比较大,一是上传速度较慢,二是前端进行渲染时速度也比较慢,比较影响客户的体验感。所以在不影响清晰度的情况下,前端可以在上传前对图片的大小体积进行压缩,压缩到一个比较合适的大小进行上传。为什么需要压缩照片大小,并介绍了压缩方法和最佳实践。
2. 技术点思路分析:
js-image-compressor是一个第三方npm库,用来图像压缩是一种减小图像文件大小的方法,从而减少加载时间和带宽消耗的技术,并优化网站加载速度,可以在不损失图像质量的前提下,优化图片文件的大小。
- ①. 可以压缩图片到指定的压缩率或特定的大小(兆字节、千字节或字节)。
- ②. 支持主流的无损和有损格式图片上传,并可以下载压缩后的JPEG、PNG和WEBP格式图片。
- ③. 可以批量压缩多张图片,将图片大小从MB迅速压缩至KB,节省时间。
4. 落地实践【方案】js-image-compressor库:
安装“js-image-compressor”库,这个库可以帮助前端进行图像压缩:
npm i js-image-compressor
这个组件允许用户上传图像,并在前端进行压缩,然后显示压缩后的图像,可以根据需要调整maxWidth、maxHeight和quality、checkOrientation等选项来控制压缩的质量和大小、图片翻转功能。
import ImageCompressor from 'js-image-compressor'
methods: {
compressFile(file) {
return new Promise((resolve, reject) => {
new ImageCompressor(file.file, {
quality: 0.6, // 压缩质量
checkOrientation: false, // 图片翻转,默认为false
success(result) {
var reader = new FileReader();
var imageItem = ''
reader.readAsDataURL(result); // readAsDataURL方法可以将上传的图片格式转为base64,然后在存入到图片路径,
reader.onload = function () {
imageItem = reader.result; // image即base64格式,后面调用后端请求传入image
resolve(reader.result)
},
error(e) {
console.log(e)
reject()
}
})
})
},
}
在上传后端OSS的时候的方法中使用即可:
methods: {
uploadImg (file) {
return new Promise(async (res, rej) => {
let compressFile = await this.compressFile(file)
let formData = new FormData()
formData.append('file', compressFile)
uploadFile(formData).then(result => {
// 判断是否是数组
file.status = 'success';
file.message = '';
file.url = result
}).catch((e) => {
console.log('错误', e)
file.status = 'failed'
file.message = '上传失败'
file.url = ''
})
})
},
}
因为公司的业务较为特殊,案件需要上传很多不同类型的报告,一个案件,最少9张,最大支持96张图片,在没有压缩之前,用户反馈这个界面操作很慢、很卡,查询了一下原因:
- 高清拍照图片大小非常大,IOS高清的有时候能达到20MB,安卓P60拍照也有10几MB。
- 业务图片会先上传到后端,后端再上传到OSS云存储,这样会产生2倍带宽。
- 后台管理有批量下载这个案件的所有图片,比较浪费带宽和下载时间。
通过安装并使用“js-image-compressor”库,在不影响画质的基础上,可以压缩图片大小,节省存储空间,并且取得很好的结果,可以看到5.3MB的图片可以压缩到745KB,压缩率达到70%,可以很好的节省OSS的存储空间、带宽流量、下载速度。
七、【场景七】:服务器工程化体系一:HTTP/2方案与OSS方案优化:
1. 原因分析:
HTTP/1.1的协议请求,并不能很好地地利用带宽,比如,一个 TCP 连接同时只能有一个 HTTP 请求和响应,如果正在发送一个 HTTP 请求,那其它的 HTTP 请求就得排队。
很多浏览器对同一域名下不同请求的并发数量都有限制,以平时经常使用的Chrome浏览器为例,当前同一个域名下,最大的并发请求限制数量为6个,如果需要等待响应的请求数量超过6个以上,那么,后面这个域名的其它请求,就会进入等待队列,只有当前面的请求完成后,才会再被执行。
2. 解决思路:
HTTP2的多路复用代替了 HTTP1.x 的序列和阻塞机制,所有的相同域名请求都通过同一个 TCP 连接并发完成,通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。
HTTP2的多路复用技术,为了解决网络中带宽越来越大而时延也居高不下,理想的消息传递方式,只要在 TCP 滑动窗口和拥塞窗口的处理范围内,发送端就应当源源不断的发送,接收端则源源不断的接收,HTTP2的多路复用代替原来的序列和阻塞机制,所有就是请求的都是通过一个 TCP 连接并发完成:
- ①. 同时也很好的解决了浏览器限制同一个域名下的请求数量的问题。
- ②. 同域名下所有通信都在单个连接上完成,同个域名只需要占用一个 TCP 连接,使用一个连接并行发送多个请求和响应。
- ③. 单个连接可以承载任意数量的双向数据流,单个连接上可以并行交错的请求和响应,之间互不干扰。
3. 【落地方案一】Nginx配置Http/2:
要让 Nginx 支持 HTTP/2,需要确保您的 Nginx 版本是 1.9.5+,因为这是引入 HTTP/2 支持的版本。此外,HTTP/2 需要 SSL/TLS 支持,因此,还需要配置 SSL 证书。
以下是一个基本的 Nginx 配置示例,展示了如何为 HTTP/2 启用 SSL:
server {
listen 443 ssl;
// 告诉 Nginx 在 443 端口上以 SSL 加密的方式启用 HTTP/2
http2 on;
server_name example.com;
// ssl_certificate 和 ssl_certificate_key 指令指定了 SSL 证书和私钥的路径,确保替换 example.com、证书路径和网站根目录路径。
ssl_certificate /path/to/test.pem;
ssl_certificate_key /path/to/ test.pem;
# 其他配置...
}
在配置好 SSL 和 HTTP/2 之后,重启 Nginx 以应用新的配置:
sudo service nginx restart
Nginx配置Http/2落地实践比较:
网页使用了许多静态资源文件,如HTML、css样式表、js脚本、图片、视频等。在HTTP1.1中由于同一个域名最多同时请求6个,导致后续的请求都在等待,网络经常是空闲的和未充分使用的,可以看到他是一个阶梯式的一个加载的过程。
而HTTP2.0可以很好的解决这个问题,同域名下所有通信都在单个连接上完成,同个域名只需要占用一个 TCP 连接,使用一个连接并行发送多个请求和响应,所以可以进行多路复用进行文件的加载,多个文件是同时进行加载,理论上带宽足够,就能传多快。
4. 【落地方案二】静态资源分散不同OSS域名加载方案:
如今互联网时代在高速发展,对网站的访问速度越来越高了,往往在图片加载的时候,会遇到卡顿、超时、缓慢的情况产生,从而需要将大量的文本类资源(如css、html、图片、txt文本)都可以通过云储存为商户实现了快捷稳定的服务。
但随着云计算,大数据,微服务技术的日趋盛起,在近几年的开发过程中,存储数据的样式种类越来越多,对数据的安全和性能的平衡等,我们需要的文件系统的特性也越来越多,不在局限于本地的文件系统存储。
随着项目的不断增长,图片或视频等文本类型的资源,也逐步由存在本地演进到存放到自己的文件服务器,后面托管到到第三方的云平台。
目前公司的项目中,全部应用于阿里云的云对象存储OSS,是一款海量、安全、低成本、高可靠的云存储服务,提供最高可达 99.995 % 的服务可用性,多种存储类型供选择,全面优化存储成本,OSS可用于图片、音视频、日志等海量文件的存储,各种终端设备、Web网站程序、移动应用可以直接向OSS写入或读取数据。
最重要的一点是:利用海量互联网带宽,OSS可以实现网页或者移动应用的静态和动态资源分离,提供原生的传输加速功能,支持上传加速、下载加速,结合CDN产品,提供静态内容存储、分发到边缘节点的解决方案,利用CDN边缘节点缓存的数据,提升访问的速度。
优化一:HPPT/2 + OSS云存储 + CDN:
比如像图片服务器,我们可能会按功能分为不同的OSS进行CDN加速,再使用CNAME映射多个域名,这样,就可以防止同一个域名最大6个的并发数据限制,另外,再加上CDN的可以分担源网站压力、避免网络拥塞,确保在不同区域、不同场景下加速网站内容的分发,提高资源访问速度。
将不同业务的静态资源分散不同OSS域名加载,再加上服务器Nginx开启了HTTP/2,可以看到不同的类型的资源,如图片、CSS、JS资源都是在同一个时间节点加载,并没有阶梯式的加载资源,同时,再配合CDN缓存在离用户更近的服务器节点上,这样可以加速降低数据传输的延迟,提高访问速度。
优化二:小程序分包与资源OSS迁移:
微信小程序的分包是指将小程序的不同功能模块分别打包成独立的代码块,以便于分布式部署和加载,对于提升小程序性能、优化用户体验至关重要。
- ①. 通过分包,小程序可以按照功能、页面等维度进行拆分,使得代码结构更清晰、维护更便捷。
- ②. 分包还可以有效减少小程序的加载时间和网络流量,提升用户体验。
最开始由于业务的需求,需要开发小程序,一些静态资源如图片、CSS、js文件是在本地存储,在最开始的小程序主包大小限制为2MB,功能是完全够用的,也没有出现问题。
但是,随着公司的业务不断的发展,功能的不断完善,功能模块的不断增加,到后面2MB肯定是不够的。
此时,将小程序的静态资源全部迁移到OSS上,再配合CDN的加速,将一些不常用的模块进行分包处理,在小程序启动时,默认会下载主包并启动主包内页面,当用户用户进入分包内某个页面时,客户端会把对应分包下载下来,下载完成后再进行展示。
优化三: OSS缩略图增效:
项目中常常会遇到加载多张图片的场景,用户不想下载每张图片的原图,如果只想大概预览这些图形的样子,还是进行原图加载,即占用带宽,又耗费用户大把时间。
在列表中使用原图加载会很卡,甚至闪退,有时候需要10几分钟才能加载完成,导致业务工作效率低下,缩略图是大图片的缩小版,即可以节省带宽,用户又可以预览缩略图来找想要的那一张图片,不用点击所有的图片,比如一些场景:
- ①. 商品列表中需要展示10几个商品,不需要显示原图。
- ②. 海报列表中需要显示所有的海报图片(往往一张海报就是10几MB),所以,只需要大概知道长什么样子的小图片代替即可,等用户真的需要下载,可以点击“海报详情”进行下载。
- ③. 后管系统浏览一个案件上传的图片,往往需要大量的拍照图片来证明用于案件的审核。
- ④. 后管列表中,商品的详情图片如果需要修改,不需要把原图显示出来,只需要显示对应的小图,因为操作人员不是每张都要编辑,只需要替换有问题的图片即可。
当用户上传到OSS上,可以通过添加后缀的形式来生成多个缩略图,这些缩略图可以用于不同的用途,如在文章列表中显示缩略图、在文章内部显示缩略图等。
将图片放到阿里云OSS上,加载列表时在图片后拼接,可以根据自己的需求选择并使用适当大小的缩略图,列表中使用缩略图加载图片会流畅很多,点击图片可以查看大图就可以了:
// 参数拼接
?x-oss-process=image/resize,m_fill,h_100,w_100
// 缩略图文件URL举例
https://oss-console-img-demo-cn-hangzhou.oss-cn-hangzhou.aliyuncs.com/example.jpg?x-oss-process=image/resize,w_300/quality,q_90。
-
①. 回源原图:对原图example.jpg添加图片缩放resize不会影响原图的任何信息。
-
②. 回源处理后的图片:对原图example.jpg添加图片缩放resize以及质量变换quality参数后。
针对OSS内存储的图片文件(Object),还可以在GetObject请求中携带图片处理参数对图片文件进行处理,例如添加图片水印、转换格式等。
将需要加速加载时间、节省存储空间、减少带宽费用或者以合适的尺寸呈现图片时,需要进行图片缩放,OSS支持通过图片缩放参数,调整Bucket内存储的图片大小。
通过上面案例展示,可以得到以下的结论:
- ①. 单张图片原图是平均2MB左右,加上缩略图后平均在5KB左右。
- ②. 单张图片原图加载时间是平均15s左右,加上缩略图后平均在300ms左右。
- ③. 整体图片原图加载是73.4MB,加上缩略图后是1.6MB,空间节省了97%左右。
- ④. 整体图片原图加载时间1.6min,加上缩略图后6.41s,时间加快了93%左右。