Search code examples
javascriptnode.jsminifyuglifyjshtml-minifier

Minify/uglify html page with embedded script tags in a nodejs app


I have an express app that, among other things, delivers some html code based on a single handlebars template. It would be great for the use-case to deliver it with short latency. So I read and compile the template when the app starts and for now I simply call

res.send(
    compiledTemplateIframeContent({ data: iframeData })
)

All of that works flawlessly.

The problem

Now I would like to minify the html and uglify the js code. I tried with html-minifier but the embedded javascript is not uglified (renaming variables), not even minified (eg remove whitespaces). I guess uglify-js is not called in the background because of some miss-configuration. I can't find my mistake by reading the docs.

I wrote some test html code and tried it in this online tool. It is just a simple gui infront of html-minifier and therefor potentially a good way to test my problem. In the online version I can at least make it compress the javascript code when I add type="text/javascript" to the tag and text/javascript to the Process scripts option (even this does not work in my express app).

But also in the online tool the Minify JavaScript checkbox does not change anything.

I don't know the possibilities of uglifyjs yet (a dependency of html-minifier) but with that name I would assume it should not only compress but also uglify the js code.

Edit / Update

I did some more research. I extracted the js code by regex match and just called uglify-js directly with the code. It had some problems with the handlebar fragments {{ }}. After removing them uglif-js works on the embedded js part of the template.

As I have set the option ignoreCustomFragments: [/{{.*}}/g] for the html-minifier I guess this should not be the problem?!

Here is the part that directly uses uglify-js

function minifyWithUglifyJs(orig: string): string {
    
    const re = /<script\b[^>]*>([\s\S]*?)<\/script>/gm;
    const match = re.exec(orig);
    if(match == null) {
        throw new Error(`Can't minify js in iframe template as found no match for js script!`);
    }

    const origJs = match[1] as string;
    console.log(origJs);
    const minifyResult = uglifyjs.minify(origJs);

    if(minifyResult.warnings != null) {
        minifyResult.warnings.forEach(logger.warn);
    }
    if(minifyResult.error) {
        throw new Error(`Can't minify the iframe template!`, {cause:minifyResult.error});
    } else {
        // replace orig js with minified js in code
        return orig.replace(origJs, minifyResult.code);
    }

}

Summing it up:

  • does anyone know what I do wrong with the http-minifier?

Here is my attempt to minify in my node app (using [email protected])

import htmlMinifier from 'html-minifier';

function minifyWithHtmlMinifier(orig: string):string {
    return htmlMinifier.minify(orig, {
        minifyJS: true,
        processScripts: ["text/javascript"],
        ignoreCustomFragments: [/{{.*}}/g]
    });
}

And here is some test code one can use in the online tool

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Test ad</title>
    </head>
    <body onload="onLoad()">
        <div id="img-container" style="width:100%;height:100%">test text in page</div>
        <script type="text/javascript">
            const longNameThatShouldBeUglified= 12;
            console.log("test");
            //{{another test}}
            function onLoad() {
                console.log("longNameThatShouldBeUglified (Not this time as it's a string) "+longNameThatShouldBeUglified);
            }
        </script>
    </body>
</html>


Solution

  • The problem is, that html-minifier does not give you any warnings when uglify-js throws any errors. I thought I had misconfigured html-minifier becaue it did not minify.

    After splitting my code to get only the content between the tags and using uglify-js directly I saw the problems and could fix the to-be-minified code. As soon as it worked I switched back to using html-minifier and it worked.

    I now use the following code where I tell html-minifier to use uglify-js wrapped in some code to recognize any problems.

    function minifyWithHtmlMinifier(orig: string):string {
        
        return htmlMinifier.minify(orig, {
    
            // I have replaced the default minifyJS usage of the html-minifier
            // as this silently ignores any uglifyjs errors and just displays
            // the non-minified code. Has cost me half a day to figure out
            // that it was not a configuration problem  but problem with the 
            // to-be-minified-code.
            // By setting the call to uglifyjs myself I can catch the errors and
            // rethrow them, failing fast, as this is much better than recognizing
            // a not-minified code much later when I don't remember what I changed
            minifyJS: (text, _inline) => { 
    
                const minifyResult = uglifyjs.minify(text, {
                    compress: {
                        drop_console: true  // remove all console log entries
                    }
                });
    
                if(minifyResult.warnings != null) {
                    minifyResult.warnings.forEach(logger.warn);
                }
                if(minifyResult.error) {
                    // TODO maybe use terser. I think it supports
                    // newer ES versions than uglifyjs
                    throw new Error(`Can't minify the iframe template!`, {cause:minifyResult.error});
                } 
                
                return minifyResult.code;
                
            },
            minifyCSS: true,
            ignoreCustomFragments: [/{{.*}}/g], // handlebars fragments
            collapseBooleanAttributes: true,
            collapseInlineTagWhitespace: true,
            collapseWhitespace: true
        });
    }
    

    Important to note: there is also html-minifier-terser that did not come up during my upfront research on what minifier to use with nodejs. Maybe it does better reporting.