性能优化之图片合并技巧

Code 代码
2020年2月9日 ~

背景

在广告落地页的场景下,由于不同广告主在搭建广告宣传页(类似使用易企秀等)过程中,操作水平各异,这个过程中会遇到某些上传大体积(达到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

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.