web-component

Web Components Performance Issue


In the last three weeks I've been switching my web app from classic HTML+CSS+JS to web components. In the begening it was all roses. So good to work with: 1. Encapsulating, 2. Reusing, 3. Less javascript code to be written, 4. Refactor one, refactor all, 5. UI clean and nice, 6. Easy to positioning and grouping. etc.

As I got excited with them, I started to change everything to web components and nesting one inside another... Then suddendly it turned into a true hell... So much more work do do in order to make the components interact with each other, use my APIs inside the shadowRoot, etc etc... Finilly I manage to make it all work together.

Here is an exemple of one of my popups:

  <s-popup id="userPanelPOP" placeholder="Painel do Usuário">
    <div class="flexCenter" loading="lazy">
      <div class="popupPageExt pdg10">
        <!-- User Panel Header -->
        <div class="w100 flex" style="height: 65px;">
          <!-- ProfilePicture Container -->
          <div class="h100 w100 flexCenter" id="profilePictureUserPanelBOX">
            <div class="rel">
              <svg-hexed-image class="profilePic"></svg-hexed-image>
              <div class="abs changePhotoBox">
                <div class="rel profilePhotoIconBanner">
                  <i class="bi bi-camera-fill abs profilePhotoIcon"></i>
                  <input class="inputFile" type="file" onchange="uploadProfilePicture(this)">
                </div>
              </div>
            </div>
          </div>
          <!-- Status Container -->
          <div class="h100 w100 flexCenter" id="statusUserPanelBOX">
            <div class="balloon" id="userPanelAuthBox">
              <i class="bi bi-exclamation-octagon-fill userPanelMenssager"></i>
            </div>
          </div>
          <!-- Logout Container -->
          <div class="h100 w100 rel">
            <s-button class="abs posTR" id="logoutUserPanelBTN" model="danger" height="30px">
              <div class="centerItemsH">
                <i class="bi bi-box-arrow-left"></i>
                <div style="min-width: 4px;"></div>
                Sair
                <div style="min-width: 1px;"></div>
              </div>
            </s-button>
          </div>
        </div>
        <!-- Welcome Menssage -->
        <s-text class="mgnTB10" id="welcomeUserPanelTXT"></s-text>
        <!-- Selling / Buying -->
        <div class="w100 flex mediaW380InlineBOX">
          <!-- My Store -->
          <div class="flexCenter flexRight w100">
            <s-button class="w100" id="myStoreUserPanelBTN" model="disable">
              <div class="centerItemsH">
                <i class="bi bi-shop fs24"></i>
                <div class="w10"></div>
                Minha Loja
              </div>
            </s-button>
          </div>
          <!-- Divisor -->
          <div class="mediaW380InlineDIV" style="min-width:10px;"></div>
          <!-- My Purchases Historic -->
          <div class="flexCenter flexLeft w100">
            <s-button class="w100" id="myPurchasesUserPanelBTN">
              <div class="centerItemsH">
                <i class="bi bi-clock-history fs24"></i>
                <div class="w10"></div>
                Minhas Compras
              </div>
            </s-button>
          </div>
        </div>
        <!-- User Informations -->
        <s-topic class="mgnT10" id="userInformationsTPC" icon="bi-arrow-down-right-square" placeholder="Informações do Usuário" display="none" style="min-width:212px">
          <div class="popupPage">
            <s-input
              class="mgnT5"
              id="nameUserPanelINP"
              placeholder="Nome Completo:"
              maxlength="80"
              charCounter="true"
              model="edit"
              regexAllow="FULL_NAME"
            >Nome Completo</s-input>
            <s-input
              id="emailUserPanelINP"
              placeholder="Email:"
              inputmode="email"
              maxlength="80"
              charCounter="true"
              model="edit"
              regexAllow="EMAIL"
            >Email</s-input>
            <div class="flex">
              <div class="w100">
                <s-input
                  id="birthUserPanelINP"
                  placeholder="Data de Nascimento:"
                  inputmode="numeric"
                  maxlength="10"
                  model="edit"
                  regexAllow="BIRTH"
                >Data de Nascimento</s-input>
              </div>
              <div class="mw10"></div>
              <div class="w100">
                <s-input
                  id="telefonUserPanelINP"
                  placeholder="Telefone:"
                  inputmode="numeric"
                  maxlength="14"
                  model="edit"
                  regexAllow="TELEFON"
                >Telefone</s-input>
              </div>
            </div>
            <!-- Gender Row -->
            <div class="flex">
              <div class="w100">
                <s-input
                  id="cpfUserPanelINP"
                  placeholder="CPF:"
                  model="edit"
                  maxlength="14"
                  inputmode="numeric"
                  regexAllow="CPF"
                >CPF</s-input>
              </div>
              <div class="mw10"></div>
              <div class="w100">
                <s-input
                  id="passwordUserPanelINP"
                  placeholder="Nova Senha:"
                  model="edit"
                  maxlength="60"
                >Senha</s-input>
              </div>
            </div>
            <s-input
              class="disNone"
              id="firstPasswordUserPanelINP"
              placeholder="Primeira Senha:"
              maxlength="60"
              type="password"
            >Primeira Senha</s-input>
            <s-input
              class="disNone"
              id="newPassword1UserPanelINP"
              placeholder="Nova Senha:"
              maxlength="60"
              type="password"
            >Nova Senha</s-input>
            <s-input
              class="disNone"
              id="newPassword2UserPanelINP"
              placeholder="Nova Senha:"
              maxlength="60"
              type="password"
            >Nova Senha</s-input>
            <div class="flex">
              <div class="w100 flexCenter">
                <s-checkbox class="mgn10" id="womanUserPanelCB" onclick="tagGenderUserPanel(this)" group="true">Mulher</s-checkbox>
              </div>
              <div class="w100 flexCenter">
                <s-checkbox class="mgn10" id="manUserPanelCB" onclick="tagGenderUserPanel(this)" group="true">Homem</s-checkbox>
              </div>
              <div class="w100 flexCenter">
                <s-checkbox class="mgn10" id="otherUserPanelCB" onclick="tagGenderUserPanel(this)" group="true">Outro</s-checkbox>
              </div>
            </div>
            <!-- User Addresses -->
            <s-topic id="userAddressesTPC" icon="bi-arrow-down-right-square" placeholder="Endereços" display="none">
              <div class="popupPage">
                <s-box placeholder="Novo Endereço" id="newAddressBOX">
                  <s-topic class="mgnT5" id="closeNewAddressUserPanelTPC" icon="bi-dash-square-dotted" placeholder="Excluir Novo Endereço">
                  <s-input
                    id="nameNewAddressUserPanelINP"
                    placeholder="Nome do Endereço (Ex.:Casa):"
                    maxlength="80"
                    charCounter="true"
                    regexAllow="FULL_NAME"
                  >Email</s-input>
                  <div class="flex">
                    <div class="w100">
                      <s-input
                        id="cepNewAddressUserPanelINP"
                        placeholder="CEP:"
                        inputmode="numeric"
                        maxlength="10"
                        regexAllow="CPF"
                      >CEP</s-input>
                    </div>
                    <div class="mw10"></div>
                    <div class="w100">
                      <s-input
                        id="stNumberNewAddressUserPanelINP"
                        placeholder="Nº do Endereço:"
                        inputmode="numeric"
                        maxlength="7"
                        regexAllow="NUMBER"
                      >Nº do Endereço</s-input>
                    </div>
                  </div>
                  <s-input
                    id="streetNewAddressUserPanelINP"
                    placeholder="Rua:"
                    maxlength="80"
                    charCounter="true"
                    regexAllow="FULL_NAME"
                  >Rua</s-input>
                  <s-input
                    id="districtNewAddressUserPanelINP"
                    placeholder="Bairro:"
                    maxlength="80"
                    charCounter="true"
                    regexAllow="FULL_NAME"
                  >Bairro</s-input>
                  <div class="flex w100">
                    <div class="w100">
                      <s-input
                        id="cityNewAddressUserPanelINP"
                        placeholder="Cidade:"
                        maxlength="40"
                        regexAllow="FULL_NAME"
                      >CEP</s-input>
                    </div>
                    <div class="mw10"></div>
                    <div style="max-width:60px;min-width:60px;">
                      <s-input
                        id="stateNewAddressUserPanelINP"
                        maxlength="2"
                        regexAllow="FULL_NAME"
                      >Estado</s-input>
                    </div>
                  </div>
                  <s-input
                    id="complementNewAddressUserPanelINP"
                    placeholder="Complemento (opcional):"
                    maxlength="80"
                    charCounter="true"
                    regexAllow="FULL_NAME"
                  >Complemento</s-input>
                  <s-button class="w100 mgnT10" id="submitNewAddressUserPanelBTN" model="disable">Cadastrar Novo Endereço</s-button>
                </s-box>
                <s-topic class="mgnT10" id="addNewAddressUserPanelTPC" icon="bi-plus-square-dotted" placeholder="Adicionar Endereço"></s-topic>
              </div>
            </s-topic>   
          </div>
        </s-topic>
        <s-input
          class="disNone"
          id="actualPasswordUserPanelINP"
          placeholder="Senha Atual:"
          maxlength="60"
          type="password"
        >Senha Atual</s-input>
        <s-button class="disNone mgnTB10" id="updateUserInfoUserPanelBTN">Atualizar Informações</s-button>
      </div>
    </div>
  </s-popup>

In the end, It took a fair amount of time to migrate completly and as I was switching to web components, I notice that the page loading was getting really slower. Google lighthouse performance score dropped from 90 to 40...

Even I tryed to lazy loading, lazy rendering, I cutted the JS into bundles and delivery them as requested, but I did not feel it was a good way to work with a progressive web app...

Now I do not know if it was worthy... I feel I lost too much time "running in circles" ending up with a much slower page...

Should I keep using web-components or just go to the old and fast way to develop an UI?


Solution

  • I found the solution!!! By doing some more research, I understand now that nesting web components is not a good ideia by using innerHTML like this:

    Old <s-popup>

    class Popup extends HTMLElement {
      constructor(){
        super();
        // Init ShadowRoot
        const shadowRoot = this.attachShadow({ mode: 'open' });
        // Add class to all popups
        this.classList.add('popup');
        this.classList.add('disNone');
        // Get tag attributes
        const id = this.getAttribute('id')??'';
        const placeholder = this.getAttribute('placeholder')??'';
        const content = this.innerHTML??'';
        // Design HTML component
        shadowRoot.innerHTML = `
          <style>
            @import url('./assets/style.min.css');
            @import url('./assets/~bootstrap-icons/font/bootstrap-icons.css');
            :host{ display: flex; }
          </style>
          <div class="popupWrap1">
            <div class="popupWrap2">
              <div class="popupWrap3 popupCloser">
                <div class="popupWrap4 popupCloser">
                  <div class="popupDialog popupFilm" onclick="event.stopPropagation();">
                    <div class="popupTitle flexCenter">
                      ${placeholder}
                      <i class="bi bi-square-fill bgSquareClose"></i>
                      <i class="bi bi-x-square-fill popupCloseIcon" onclick="closePopup(${id});"></i>
                      <div class="popupReturn flexCenter"></div>
                      <div class="titleBottomBorder"></div>
                    </div>
                    ${content}
                  </div>
                </div>
              </div>
            </div>
          </div>`;
          // Popup closer
          this.shadowRoot.querySelector('.popupCloser').onclick = e =>{ closePopup(this); }
      }
      hideNavBars(){
        this.shadowRoot.querySelector('.popupWrap1').style.marginTop = '0px';
        this.shadowRoot.querySelector('.popupWrap1').style.marginBottom = '0px';
      }
      showNavBars(){
        this.shadowRoot.querySelector('.popupWrap1').style.marginTop = nbHeaderHeight + 'px';
        this.shadowRoot.querySelector('.popupWrap1').style.marginBottom = nbFooterHeight + 'px';
      }
    }
    customElements.define('s-popup', Popup);
    

    but simply using <slot> tag instead of innerHTML did the trick and now the page is loading blazingly fast!

    New <s-popup>

    class Popup extends HTMLElement {
      constructor(){
        super();
        // Init ShadowRoot
        const shadowRoot = this.attachShadow({ mode: 'open' });
        // Add class to all popups
        this.classList.add('popup');
        this.classList.add('disNone');
        // Get tag attributes
        const id = this.getAttribute('id')??'';
        const placeholder = this.getAttribute('placeholder')??'';
        // Design HTML component
        shadowRoot.innerHTML = `
          <style>
            @import url('./assets/style.min.css');
            @import url('./assets/~bootstrap-icons/font/bootstrap-icons.css');
            :host{ display: flex; }
          </style>
          <div class="popupWrap1">
            <div class="popupWrap2">
              <div class="popupWrap3 popupCloser">
                <div class="popupWrap4 popupCloser">
                  <div class="popupDialog popupFilm" onclick="event.stopPropagation();">
                    <div class="popupTitle flexCenter">
                      ${placeholder}
                      <i class="bi bi-square-fill bgSquareClose"></i>
                      <i class="bi bi-x-square-fill popupCloseIcon" onclick="closePopup(${id});"></i>
                      <div class="popupReturn flexCenter"></div>
                      <div class="titleBottomBorder"></div>
                    </div>
                    <slot></slot>
                  </div>
                </div>
              </div>
            </div>
          </div>`;
          // Popup closer
          this.shadowRoot.querySelector('.popupCloser').onclick = e =>{ closePopup(this); }
      }
      hideNavBars(){
        this.shadowRoot.querySelector('.popupWrap1').style.marginTop = '0px';
        this.shadowRoot.querySelector('.popupWrap1').style.marginBottom = '0px';
      }
      showNavBars(){
        this.shadowRoot.querySelector('.popupWrap1').style.marginTop = nbHeaderHeight + 'px';
        this.shadowRoot.querySelector('.popupWrap1').style.marginBottom = nbFooterHeight + 'px';
      }
    }
    customElements.define('s-popup', Popup);
    

    simple like that!

    The reason is when nesting web components using innerHTML, every time we render each inner-nested-component, the whole DOM needs to be updated as innerHTML manipulates directly the DOM. But by using <slot> instead, the browser creates a Virtual DOM, then renders the page inside of it using a some sort of fast memory allocation and in the end, it just does a simple and single update to the DOM. Now it is working like a charm!