Covert html to pdf, calculate the vertical space to the end of the last page faster I'm working on a custom puppeteer script which converts dynamic generated HTML to an A4 PDF.
This script converts HTML to a near perfect A4 PDF file, the only issue I have is speed. Document conversion is anywhere between 1-10 minutes.

I have tracked the slowness to the part where we calculate how much vertical space needs to be added to the last page
in order to have a full page. If we don't do this the background we have will be clipped.

Next up the relevant code, which is called as followed;

node convert.js "{\"url\":\"file://$(pwd)/generated.html\",\"options\":{\"path\":\"$(pwd)/generated.pdf\"}}"


async function addPadding(page) {
    await page.emulateMedia('print');

    await page.evaluate(
        _ =&gt; {
            let container = document.querySelector('#padding-container');

            if (container)
                container.style.height = '1145px'
        }
    );

    return Promise.resolve()
}

function getPageAmount(buffer) {
    let index = buffer.indexOf('/Count ');
    let string = buffer.slice(index + '/Count '.length, index + '/Count '.length + 32).toString();
    return ~~string.trim().split(/\n|\/|\[|\&gt;/)[0]
}


convert.js

(async _ =&gt; {
    const browser = await puppeteer.launch({
        headless: true,
        args: [
            isDocker ? '--no-sandbox' : ''
        ],
        defaultViewport: {
            width: 724,
            height: 1145 * 32
        }
    });
    const page  = await browser.newPage();

    await page.emulateMedia('print');
    await page.goto(request.url, { waitUntil: 'networkidle0' });
    await page.setViewport({ width: 724, height: 1145 });

    let buffer = await render(page);
    let originalPages = getPageAmount(buffer);
    let maxPasses = 3;
    let passes = 0;

    await addPadding(page);

    buffer = await render(page);

    while(getPageAmount(buffer) !== originalPages &amp;&amp; passes &lt; maxPasses) {
        await page.evaluate(
            _ =&gt; {
                let paddingContainer = document.querySelector('#padding-container');
                let value = ~~paddingContainer.style.height.replace('px', '');
                paddingContainer.style.height = `${value / 2}px`
            }
        );

        buffer = await render(page);

        if (getPageAmount(buffer) === originalPages) {
            while (getPageAmount(buffer) === originalPages) {
                await page.evaluate(
                    _ =&gt; {
                        let paddingContainer = document.querySelector('#padding-container');
                        let value = ~~paddingContainer.style.height.replace('px', '');
                        paddingContainer.style.height = `${value * 1.5}px`
                    }
                );

                buffer = await render(page)
            }
        }

        passes++
    }

    while(getPageAmount(buffer) !== originalPages) {
        await page.evaluate(
            _ =&gt; {
                let paddingContainer = document.querySelector('#padding-container');
                let value = ~~paddingContainer.style.height.replace('px', '');
                paddingContainer.style.height = `${value - 1}px`
            }
        );

        buffer = await render(page)
    }

    if (request.options.path)
        fs.writeFileSync(request.options.path, buffer);

    browser.close()
})();


I really would appreciate any feedback/help on this.
Ways to optimize:

Nested loops

You got nested while loops in convert.js script and that requires a particular attention.The outer while loop contains the construction:

...
if (getPageAmount(buffer) === originalPages) {
    while (getPageAmount(buffer) === originalPages) {
...


where if conditional redundantly checks the condition getPageAmount(buffer) === originalPages whereas the underling while loop would check the same condition by itself. Therefore, remove the redundant if "wrapper".



getPageAmount function

Deserves a separate attention (frequently invoked function).


'/Count '. Many-times hardcoded search string '/Count ' begs for extracting into a variable

let searchStr = '/Count ';

index + '/Count '.length. Duplicated expression points to a starting offset for input buffer slicing. Worth to be a variable:

let pos = buffer.indexOf(searchStr);
let startOffset = pos + searchStr.length;
let str = buffer.slice(startOffset, startOffset + 32).toString();

splitting a string by pattern and get the 1st chunk (~~string.trim().split(/\n|\/|\[|\&gt;/)[0]). What it does is splitting the input string by regex pattern /\n|\/|\[|\&gt;/ into array of substrings. Though it creates a new array of strings/chunks in memory - whereas we only need the 1st leftmost chunk .[0].Instead, a much more efficient way is to just find the position of the 1st occurrence of the pattern and slice the input string to that point.That's achievable with String.search + String.slice combination and will go smashingly faster compared to the initial approach.
Eventually the optimized function would look as:

function getPageAmount(buffer) {
    let searchStr = '/Count ',
        pos = buffer.indexOf(searchStr),
        startOffset = pos + searchStr.length;
    let str = buffer.slice(startOffset, startOffset + 32).toString();
    return ~~str.trim().slice(0, str.search(/[\n\/\[\&gt;]/))
}



DOM tree scanning

The "hero" of this section is document.querySelector('#padding-container') which appears in many places within while loops and queries the current document for a specific tag/element.
Such DOM queries become an expensive operations if used frequently, moreover - in massive traversals. Depending on markup complexity and "amount" of traversal such repetative queries may make the processing +50% slower.The solution here is to extract the reference to an element into a top-level variable and reference it in all needed places. 

# top-level variables
...
let passes = 0;
let paddingContainer = document.querySelector('#padding-container');


Extracting "padding container" height

Expression ~~paddingContainer.style.height.replace('px', '') is duplicated in many places and is candidate for Extract function technique.Could be even defined as unified function for getting height for the element passed as parameter:

function getElHeight(el):
    return ~~el.style.height.replace('px', '')

...

    ...
    _ =&gt; {
        let padding_height = getElHeight(paddingContainer);
        paddingContainer.style.height = `${padding_height * 1.5}px`
    }