性能优化之图片合并技巧

Code 代码
2月 09, 2020 ~

背景

在广告落地页的场景下,由于不同广告主在搭建广告宣传页(类似使用易企秀等)过程中,操作水平各异,这个过程中会遇到某些上传大体积(达到1-2mb)的png格式图片,或者有些用户搭建页面时会使用特别碎片化的图片(大部分用户是psd导出的宣传图片,常常由于图层问题,一张完整的图被分割成多张碎小图),有些页面甚至存在200多张图片,实际完整图片却只有10张左右。

这样带来的后果是:

a. 图片大小过大造成了页面加载过程中,白屏时间过长,如果网速不佳情况下,容易导致用户的流失

b. 数量过多,占用了请求数量的个数限制,有可能导致某些动态拉取的js sdk 延后加载。

在这种操作各异,且不符合优化最佳实践的情况下,我们需要一种方案能够抹平不同操作人员在搭建广告宣传页的不同。

方案

页面的自定义搭建,通常是由一堆组件构成,并会生成一份大的Json Schema来描述不同组件的位置信息

描述文件:

...
{
    "x": 20,
    "y": 20,
    "bgType": {
        "title": "背景类型",
        "type": "string",
        "enum": ["color", "image"],
        "default": "color"
    },
    "bgColor": {
        "title": "填充颜色",
        "$ref": "color.json",
        "default": {
            "color": "#4E90FF"
        }
    },
    "bgImage": {
        "title": "背景图片",
        "$ref": "image.json",
        "url": "x.png",
        "size": "1024000",
        "width": "960",
        "height": "320"
    }
}
...

如图:

合并前的问题:

什么样的组件可以合并?

  • 由于组件的位置是有层级关系的,会导致合并前后的层级错乱

如:合并前

合并后:

我们称为夹心组件,对于这样夹心的情况,如果是简单组件我们可以把文字也变成一张图,绘制出这样的情况,或者对复杂组件或者除图片外的组件过滤,只合并图片

  • 在有些情况下,如首屏外的下方区域图片数量过多,这样合并出来的图片会导致首屏所需图片的大小增加,而用户这段等待的时间又是无效时间,对于首屏速度是个极大的指标影响

合并方案选择

1、puppeteer截图

图片合并可以在用户保存图片后经过截图服务,对指定的区域截图上传到图片cdn生成一张新图

puppeteer是一个node库, 可以在服务器端起一个headless Chrome 无头浏览器,通过API来模拟用户操作chrome访问页面,通常用于截图或者UI Test或者爬虫访问,自动化测试,页面劫持方面

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch({
        headless: false
    });
    const page = await browser.newPage();
    await page.goto('https://www.xxx.com/test.html');
    await page.setViewport({
        width: 1200,
        height: 800
    });
    //获取页面Dom对象
    let body = await page.$('#post_body');
    //调用页面内Dom对象的 screenshot 方法进行截图
    await body.screenshot({
        path: '2.png'
    });
    await browser.close();
})();

这个方案看起来最简单可行,简直太方便了,只需要轻轻一按就能截图,不管什么夹心都能处理,简单又便捷,但是...

请求到达->启动Chromium->打开tab页->截图->关闭tab页->关闭Chromium->返回数据

Chromium Headless是吃内存大户,使用puppeteer难免有些大财小用

Chromium消耗最多的资源是CPU,一是渲染需要大量计算,二是Dom的解析与渲染在不同的进程,进程间切换会给CPU造成压力(进程多了之后特别明显)。其次消耗最多的是内存,Chromium是以多进程的方式运行,一个页面会生成一个进程,一个进程占用30M左右的内存,大致估算1000个请求占用30G内存,在并发高的时候内存瓶颈最先显现。

在测试过程中,进行了一系列优化后,如机器启动就初始化若干个browser,请求打过来时,直接在browserList中取一个browser实例使用,一堆的browser复用 page复用 tab标签页复用,并发来到了“可喜”的单实力 1 qps...

2. canvas绘制 node-canvas

由于在JSON的描述信息中,我们得知了图片的Url,x, y位置信息,宽高大小等信息

const { createCanvas, loadImage } = require('canvas')
const canvas = createCanvas(200, 200)
const ctx = canvas.getContext('2d')

loadImage('url').then((image) => {
  ctx.drawImage(image, x, y, width, heighten )

  console.log('<img src="' + canvas.toDataURL() + '" />')
})
canvas.toBuffer()

我们可以很轻易的用canvas去绘制图片,但是除了前面提到的对页面的首屏图片优先合并的策略外,有些特殊情况需要处理

处理特殊情况: 拉伸图的处理

某些图片作为背景图片情况存在,是会对原图片比例进行拉伸的,cover背景图,图片原比例是 980 * 1200 而如果背景大小是300 * 400 图片设置为cover背景图(参考css中的 backgroud repeat cover 等属性),那么我们需要根据图片的比例进行拉伸缩放

private async getBgImageUnit(): Promise< IUnit | undefined> {
        if (this.data.bgImage.src && this.data.bgType === 'image') {
            // 背景模块大小,需要拉伸的大小
            const { width: cWidth, height: cHeight } = this.data;
            // 图片本身大小
            const { width: pWidth, height: pHeight, src } = this.data.bgImage;
            // DPI
            const { DPI } = this.config;

            const canvas = createCanvas(cWidth * DPI, cHeight * DPI);
            const ctx = canvas.getContext('2d');
            const Image = await loadImage(src);
            const imageRatio = pWidth / pHeight;
            const canvasRatio = cWidth / cHeight;

            let newX: number;
            let newY: number;
            let newHeight: number;
            let newWidth: number;
            
            // 判断是需要横向拉伸还是竖向拉伸,模块宽高比例是否大于图片宽高比例,比例不同拉伸方向不同
            if (imageRatio < canvasRatio) {
                newWidth = pWidth;
                newHeight = newWidth / canvasRatio;
                newX = 0;
                newY = (pHeight - newHeight) / 2;
            } else {
                newHeight = pHeight;
                newWidth = pHeight * canvasRatio;
                newY = 0;
                newX = (pWidth - newWidth) / 2;
            }
            ctx.drawImage(Image, newX, newY, newWidth, newHeight, 0, 0, cWidth * DPI, cHeight * DPI);
            const buffer = canvas.toBuffer();

            const { name, x, y, width, height, image } = this.data;

            return {
                name,
                x,
                y,
                width,
                height,
                color: '',
                type: 'coverImage',
                image: { ...image, data: buffer }
            };
        }
    }

通过canvas我们还可以轻易的控制图片输出的质量,格式,除了作为合并图片的服务。还属于一个图片质量的性能开关,在服务降级的情况下,可以将所有图片的质量转为jpeg,减小带宽压力

标签

Cuzz

handsome boy