richardtallent / vite-plugin-singlefile

Vite plugin for inlining JavaScript and CSS resources
MIT License
859 stars 58 forks source link

Idea: Remove unused Vite module loader #9

Closed Greenheart closed 2 years ago

Greenheart commented 3 years ago

Hi! First of all, thanks again for creating this plugin! I've used it for 4 projects all with great success! 🎉

TL;DR: Tiny optimization suggestion to reduce the build output by ~ 1kb (≈ 23% in my example project). Feel free to close the issue if it's out of scope!


Background

When importing any kind of JS modules, vite build automatically adds a minimal import wrapper in the front of the build output. This code seems to handle importing "modules" by adding script tags.

Notice how it's bound to the '/assets/' directory as a root path for loading new modules. However, we don't load any modules from '/assets/' since everything is inlined into index.html when using vite-plugin-singlefile.

!(function (e = '.', t = '__import__') {
    try {
        self[t] = new Function('u', 'return import(u)')
    } catch (r) {
        const n = new URL(e, location),
            o = (e) => {
                URL.revokeObjectURL(e.src), e.remove()
            }
        ;(self[t] = (e) =>
            new Promise((r, a) => {
                const i = new URL(e, n)
                if (self[t].moduleMap[i]) return r(self[t].moduleMap[i])
                const s = new Blob(
                        [
                            `import * as m from '${i}';`,
                            `${t}.moduleMap['${i}']=m;`,
                        ],
                        { type: 'text/javascript' }
                    ),
                    c = Object.assign(document.createElement('script'), {
                        type: 'module',
                        src: URL.createObjectURL(s),
                        onerror() {
                            a(new Error(`Failed to import: ${e}`)), o(c)
                        },
                        onload() {
                            r(self[t].moduleMap[i]), o(c)
                        },
                    })
                document.head.appendChild(c)
            })),
            (self[t].moduleMap = {})
    }
})('/assets/')
Raw module loader snippet !function(e=".",t="__import__"){try{self[t]=new Function("u","return import(u)")}catch(r){const n=new URL(e,location),o=e=>{URL.revokeObjectURL(e.src),e.remove()};self[t]=e=>new Promise(((r,a)=>{const i=new URL(e,n);if(self[t].moduleMap[i])return r(self[t].moduleMap[i]);const s=new Blob([`import * as m from '${i}';`,`${t}.moduleMap['${i}']=m;`],{type:"text/javascript"}),c=Object.assign(document.createElement("script"),{type:"module",src:URL.createObjectURL(s),onerror(){a(new Error(`Failed to import: ${e}`)),o(c)},onload(){r(self[t].moduleMap[i]),o(c)}});document.head.appendChild(c)})),self[t].moduleMap={}}}("/assets/");

The idea

Maybe we could remove the vite module loader?

I've tested it in one of my (tiny) projects and managed to reduce the JS output by 23% (~ 1kb).

Note: This project is currently in a major rewrite, so it's not the ideal reproduction environment. But at least the steps below explain the process:

  1. Run npm run build in the pagecrypt project
  2. Open /web/build/index.html and find the main script type="module" in the <head> section.
  3. Remove the raw module loader snippet (available above) from the build JS output.
  4. run npm run serve and see that the page still works on localhost:5000
JS output after building with vite-plugin-singlefile (3012 bytes) var e,t;!function(e=".",t="__import__"){try{self[t]=new Function("u","return import(u)")}catch(r){const n=new URL(e,location),o=e=>{URL.revokeObjectURL(e.src),e.remove()};self[t]=e=>new Promise(((r,a)=>{const i=new URL(e,n);if(self[t].moduleMap[i])return r(self[t].moduleMap[i]);const s=new Blob([`import * as m from '${i}';`,`${t}.moduleMap['${i}']=m;`],{type:"text/javascript"}),c=Object.assign(document.createElement("script"),{type:"module",src:URL.createObjectURL(s),onerror(){a(new Error(`Failed to import: ${e}`)),o(c)},onload(){r(self[t].moduleMap[i]),o(c)}});document.head.appendChild(c)})),self[t].moduleMap={}}}("/assets/");var r={chars:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",bits:6},n=function(e,t){return function(e,t,r){if(void 0===r&&(r={}),!t.codes){t.codes={};for(var n=0;n=8&&(i-=8,a[c++]=255&s>>i)}if(i>=t.bits||255&s<<8-i)throw new SyntaxError("Unexpected end of data");return a}(e,r,t)};const o=document.querySelector.bind(document),[a,i,s,c,d,l]=["input","iframe","header","#msg","#locked","#unlocked"].map(o);""===window.pl&&(a.disabled=!0,g("No encrypted payload."));const u=n(window.pl),w=u.slice(0,32),m=u.slice(32,48),p=u.slice(48),f=(null==(e=window.crypto)?void 0:e.subtle)||(null==(t=window.crypto)?void 0:t.webkitSubtle);function h(e){e.classList.remove("hidden")}function v(e){e.classList.add("hidden")}function g(e){c.innerText=e,s.classList.toggle("text-red-600",!0),s.classList.toggle("text-green-600",!1),h(d),v(l)}window.crypto.subtle||(g("Please use a modern browser."),a.disabled=!0),o("form").addEventListener("submit",(async e=>{e.preventDefault();try{t="Decrypting...",c.innerText=t,s.classList.toggle("text-green-600",!1),s.classList.toggle("text-red-600",!1),h(d),v(l),await async function(e){return new Promise((t=>setTimeout(t,e)))}(60);const e=await async function({salt:e,iv:t,ciphertext:r},n){const o=new TextDecoder,a=new TextEncoder,i=await f.importKey("raw",a.encode(n),"PBKDF2",!1,["deriveKey"]),s=await f.deriveKey({name:"PBKDF2",salt:e,iterations:1e6,hash:"SHA-256"},i,{name:"AES-GCM",length:256},!1,["decrypt"]),c=new Uint8Array(await f.decrypt({name:"AES-GCM",iv:t},s,r));return o.decode(c)}({salt:w,iv:m,ciphertext:p},a.value);if(!e)throw"Malformed data";!function(e){c.innerText=e,s.classList.toggle("text-green-600",!0),s.classList.toggle("text-red-600",!1),h(l),v(d)}("Success!"),i.srcdoc=e.replace("",''),window.setTimeout((()=>{o("main").remove(),h(i),document.querySelectorAll("script").forEach((e=>e.remove()))}),1e3)}catch(r){g("Wrong password."),a.value=""}var t}));
JS output after removing module loader (2381 bytes - 23% less code) var e,t,r={chars:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",bits:6},n=function(e,t){return function(e,t,r){if(void 0===r&&(r={}),!t.codes){t.codes={};for(var n=0;n=8&&(i-=8,a[c++]=255&s>>i)}if(i>=t.bits||255&s<<8-i)throw new SyntaxError("Unexpected end of data");return a}(e,r,t)};const o=document.querySelector.bind(document),[a,i,s,c,d,l]=["input","iframe","header","#msg","#locked","#unlocked"].map(o);""===window.pl&&(a.disabled=!0,g("No encrypted payload."));const u=n(window.pl),w=u.slice(0,32),m=u.slice(32,48),p=u.slice(48),f=(null==(e=window.crypto)?void 0:e.subtle)||(null==(t=window.crypto)?void 0:t.webkitSubtle);function h(e){e.classList.remove("hidden")}function v(e){e.classList.add("hidden")}function g(e){c.innerText=e,s.classList.toggle("text-red-600",!0),s.classList.toggle("text-green-600",!1),h(d),v(l)}window.crypto.subtle||(g("Please use a modern browser."),a.disabled=!0),o("form").addEventListener("submit",(async e=>{e.preventDefault();try{t="Decrypting...",c.innerText=t,s.classList.toggle("text-green-600",!1),s.classList.toggle("text-red-600",!1),h(d),v(l),await async function(e){return new Promise((t=>setTimeout(t,e)))}(60);const e=await async function({salt:e,iv:t,ciphertext:r},n){const o=new TextDecoder,a=new TextEncoder,i=await f.importKey("raw",a.encode(n),"PBKDF2",!1,["deriveKey"]),s=await f.deriveKey({name:"PBKDF2",salt:e,iterations:1e6,hash:"SHA-256"},i,{name:"AES-GCM",length:256},!1,["decrypt"]),c=new Uint8Array(await f.decrypt({name:"AES-GCM",iv:t},s,r));return o.decode(c)}({salt:w,iv:m,ciphertext:p},a.value);if(!e)throw"Malformed data";!function(e){c.innerText=e,s.classList.toggle("text-green-600",!0),s.classList.toggle("text-red-600",!1),h(l),v(d)}("Success!"),i.srcdoc=e.replace("",''),window.setTimeout((()=>{o("main").remove(),h(i),document.querySelectorAll("script").forEach((e=>e.remove()))}),1e3)}catch(r){g("Wrong password."),a.value=""}var t}));

Let me know if we need a better demo to reproduce and test this.

Why?