"𝙎𝙢𝙖𝙡𝙡𝙚𝙨𝙩𝙚𝙙𝙞𝙣𝙜" : making a Web Component 29% smaller

Native code ranks first in the File Size Ranking

But... I am totally against comparison with Svelte, because the Svelte compiler optimizes code

So if I "play compiler"

That native HTMLElement code can/may be optimized
(and so can all other source versions)

gzip
original by WebComponents DEV  476 B*
Svelte 1884 B
refactored  355 B - savings: 25%
optimized  339 B - savings: 29%

*) The Web Components DEV site says the file is 505 Bytes,
copied to GitHub.io my <file-size> Web Component reports it is 476 Bytes.

Original <my-counter> = 476 Bytes:

Refactor code

See JavaScript Tab in above JSFiddle.

  • Template literals are great, but they suck up bytes, as meaningless white space and \n newlines are still included in the minified file.

  • no need for a createElement('template') when we only want the innerHTML once

  • template/content should not be added in the connectedCallback (as it can run multiple times)

  • super() sets and returns the this scope

  • attachShadow() sets and returns this.shadowRoot

  • so everything can be chained:

        constructor() {
            super()
                .attachShadow({ mode: 'open' })
                .innerHTML = "<style>*{font-size:200%}...
    
  • no need for a MyCounter class definition when it is used only once

        customElements.define('my-counter', class extends HTMLElement {}
    
  • Nearly all of the 51 examples use inline event handlers (notation).

        render() {
            return html`
            <button @click="${this.dec}">-</button>
            <span>${this.count}</span>
            <button @click="${this.inc}">+</button>
            `;
        }
    
  • Then we can do that as well

    • we have to add extra code to find the inc() and dec() methods on the element (which libraries do for you under the hood)
    • the id references on the buttons are no longer needed
        <button onclick="this.getRootNode().host.inc()">
        <button onclick="this.getRootNode().host.dec()">
    
  • The Component uses shadowRoot to encapsulate styles and content. The id on <span id="count"> is not required because we can target the only <span> that exists in shadowDOM

  • All the refactored connectedCallback does is set the span innerHTML to 0

        connectedCallback() {
            this.update(this.count);
        }
    

    Set the default 0 in HTML, and the connectedCallback is no longer required

        "<span>0</span>"+
    
  • remove not required white space and ; from CSS

  • remove not required quotes from HTML attributes, because the Browser will add them

        <button onclick=this.getRootNode().host.inc()>
        <button onclick=this.getRootNode().host.dec()>
    

Refactored code = 355 Bytes:

customElements.define("my-counter", class extends HTMLElement {
    constructor() {
      super()
        .attachShadow({ mode: "open" })
        .innerHTML =
        "<style>" +
        "*{font-size:200%}"+
        "span{width:4rem;display:inline-block;text-align:center}" +
        "button{width:4rem;height:4rem;border:none;border-radius:10px;background-color:seagreen;color:white}" +
        "</style>" +
        "<button onclick=this.getRootNode().host.dec()>-</button>" +
        "<span>0</span>" +
        "<button onclick=this.getRootNode().host.inc()>+</button>";
      this.count = 0;
    }
    inc() {
      this.update(++this.count);
    }
    dec() {
      this.update(--this.count);
    }
    update(count) {
      this.shadowRoot.querySelector("span").innerHTML = count;
    }
  }
);

Optimized code = 339 Bytes:

This Component can be made better and even smaller

  • The inc, dec and update methods are not required, when count is made a getter/setter

  • DRY (Don't Repeated Yourself) is great from a code maintenance Point-of-View. But from a delivery and performance PoV you do not want to be DRY; GZip loves repetitions
    * {font-size:200%} is applied to 2 elements only (button and span)
    Setting font-size:200% on both elements creates a larger file, but a smaller GZipped file!
    (And the CSS parser has less work to do)

  • .count-- > needs that extra space, to close the onclick definition, or the minifier will add an - escape code, adding 4 bytes.

  • <span> can be replaced with <p>

  • no this.count = 0; required because <p>0</p> is the state

  • replacing seagreen and white with shorter #xxx notation doesn't save extra bytes in this case, because the # doesn't exist yet in the code, thus requires extra GZip encoding bits.

customElements.define(
  "my-counter",
  class extends HTMLElement {
    constructor() {
      super().attachShadow({
        mode: "open",
      }).innerHTML =
        "<style>" +
        "p{font-size:200%;width:4rem;display:inline-block;text-align:center}" +
        "button{font-size:200%;width:4rem;height:4rem;border:none;border-radius:10px;background:seagreen;color:white}" +
        "</style>" +
        "<button onclick=this.getRootNode().host.count-- >-</button>" +
        "<p>0</p>" +
        "<button onclick=this.getRootNode().host.count++>+</button>";
    }
    set count(p) {
      this.shadowRoot.querySelector("p").innerHTML = p;
    }
    get count() {
      return ~~this.shadowRoot.querySelector("p").innerHTML;
    }
  }
);

Note: You can save 6 more bytes using INline styles
but code maintainability suffers:

.innerHTML = 
"<button style=font-size:200%;width:4rem;height:4rem;border:none;border-radius:10px;background:seagreen;color:white onclick=this.getRootNode().host.count-- >-</button>" +
"<p style=font-size:200%;width:4rem;display:inline-block;text-align:center>0</p>" +
"<button style=font-size:200%;width:4rem;height:4rem;border:none;border-radius:10px;background:seagreen;color:white onclick=this.getRootNode().host.count++>+</button>"

smallesteding-ed Conclusion

Refactoring Web Components makes them smaller and better.

gzip
original by WebComponents.DEV  476 B*
Svelte 1884 B
refactored  355 B - savings: 25%
optimized  339 B - savings: 29%

17