Skip to content

Instantly share code, notes, and snippets.

@Jliv316
Last active December 7, 2021 15:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Jliv316/fd030c68cdc94213fa39c2521c9a6e23 to your computer and use it in GitHub Desktop.
Save Jliv316/fd030c68cdc94213fa39c2521c9a6e23 to your computer and use it in GitHub Desktop.
Page Speed Optimization (Webpack)

Page Speed Optimization

Page Speed Insights

Using googles page speed insights tool we get a break down of a first time mobile user's experience using various metrics. https://pagespeed.web.dev/report?utm_source=psi&utm_medium=redirect&url=https%3A%2F%2Fqa.landscapehub.com%2F

image

This test emulates a mobile user on 4g network. Some of the more critical metrics are:

  1. Total blocking time
  2. Time to interactive
  3. Largest contentful paint

As you can see by the test we fail in almost every category. The overall performance score is broken into several different metrics with each metric having varying weight

image

The report then shows your some of the ways you can improve these perforance scores in your app. Below you can see that the biggest improvement can be made by reducing unused javascript.

image

Reproducing Locally

If you look at the network tab and select "js", we can see the number and size of js being sent to the user upon hitting the MarketingHomePage. Here we see that the client-app-bundle is the culprit weighing in at a whopping 25.8 MB.

image

Our Webpack / ReactOnRails Configurations

Our webpack.config.js file shows that we are packaging our entire app (outside of beehive admin pages) into one enormous bundl. This means that regardless of what page the user is trying to access that they will load assets for all pages in our app.

const shims = ["@babel/polyfill"];

const appEntry = "./client/app/startup/registration.js";
const adminEntry = "./client/beehive/startup/registration.js";

entry: {
    shims: shims,
    "new-user-tracker": "./client/new-user-tracker/entry.js",
    "marketing-site": "./client/marketing-site/entry.js",
    "client-app-bundle": [].concat("./client/app/client-entry", appEntry),
    "client-admin-bundle": [].concat("./client/app/client-entry", adminEntry),
    "server-bundle": [].concat(shims, appEntry, adminEntry)
  },

Above are the entry points for webpack bundling. Marketing-site only contains the landscapeHub marketing video. appEntry and adminEntry is the path to our registration.js files which registers our React.js components with ReactOnRails.

ReactOnRails registration works closely with Webpack to then bundle these front end components.

The way that we gain access to these bundles in our views is using ReactOnRails helper method javascript_pack_tag {pack_name}

We can see this in our app/views/layouts/application/_foot.html.haml view

-# -----------------------------------------------------------------------------
-# Javascripts
-# -----------------------------------------------------------------------------

-# Include files generated by React on Rails via Webpack
= javascript_pack_tag "shims"
= javascript_pack_tag "marketing-site"
= javascript_pack_tag "client-app-bundle"

-# All the other scripts (including jQuery).
= javascript_include_tag "application-foot"

/[if (lte IE 9)]
  -# Loads selectivizr and respond
  = javascript_include_tag "application-foot-ltie10"

-# Deferred JS scripts. These don't run until after the load event is triggered.
= defer_javascript_include_tag "application-defer.js"

- if content_for? :page_scripts
  = yield :page_scripts

= render("layouts/application/svg_sprite_sheet")

The size of the javascript pack, or bundle, is determined by the components registered in the registration.js file as well as the dependent code and node modules associated with that registered component.

I found a package that will help me take a look at what all is included in our bundle, client-app-bundle, called webpack-bundle-analyzer (WBA).

The outputted html file from WBA shows that the bundle is ~25 MB with a majority of it being node-modules. image

Let's zoom in on client-app-bundle since this is the bundle we're most interested in reducing in size.

image

CodeSplitting and Chunking Bundles

Since the majority of the bunle was node-modules I figured we might as well try and separate them into their own bundle called vendor.{hash}.js. By adding this opimization snippet to our module.exports config in webpack.config.js we can effectively do this.

optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          chunks: 'all'
        }
      }
    },
  },

If we look at our WBA bundle map below we can see that we successfully moved our node-modules into their own bundle. This means that all of our bundles now reference and share the common code, node modules in this case, see in vendor.js

image

Yay! It looks like we were able to reduce our client-app-bundle by over 50% going from ~25 MB to ~10 MB.

Oh..... wait... merrr

image

Looks like I'm not the only one running into this error: reactjs/react-rails#970 rails/webpacker#2270

Solution: We sucessfully removed node-modules from the client-app-bundle but some of our components are server side rendered and need these node-modules to fully render their respective html pages before sending to client.

Okay, so let's move all server side rendered components into their own registration file and entry point and make sure we do not chunk those bundles.

const appEntry = "./client/app/startup/registration.js";
const adminEntry = "./client/beehive/startup/registration.js";
const appPrerenderEntry = "./client/app/startup/server-rendering.js";

entry: {
    shims: shims,
    "new-user-tracker": "./client/new-user-tracker/entry.js",
    "marketing-site": "./client/marketing-site/entry.js",
    "client-app-bundle": [].concat("./client/app/client-entry", appEntry),
    "server-rendering": [].concat("./client/app/client-entry", appPrerenderEntry),
    "client-admin-bundle": [].concat("./client/app/client-entry", adminEntry),
    "server-bundle": [].concat(shims, appEntry, adminEntry, appPrerenderEntry)
  },

So you can now see I added the appPrerenderEntry and server-rendering entry point. I also add below the helper function to filter out bundles containing SSR pages.

const notServerRendering = name => !name.includes("server");

And finally added the helper to the optimization config

optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          chunks(chunk) {
            return notServerRendering(chunk.name);
          }
        }
      }
    },
  },

Let's run webpack again and see if we made it past that error. Looks good, now let's see what our webpack analyzer has to tell us:

image

We reduced the client-app-bundle down to 9 MB!! BUT, our server-rendered bundle is 20 MB. Since MarketingHomePage is an SSR page and that's the first page our new users will hit... this is still quite large. To put it in perspective, webpack considers anything over 1 MB to be a BIG bundle.

So how about we move MarketingHomePage into its own bundle and move it to a different layout so server-rendering and client-app-bundle don't get sent over when a user hits the marketing home page.

Well.... actually... let's check and see how big MarketingHomePage is by itself before we do all this work ^^^^

I edited the server registration file from this

import "./polyfills";
import ReactOnRails from "react-on-rails";
import HealthCheck from "../components/HealthCheck";
import PurchaseOrderAlreadyAcknowledged from "../bundles/PurchaseOrderResponsePages/components/PurchaseOrderAlreadyAcknowledged";
import PurchaseOrderConfirmError from "../bundles/PurchaseOrderResponsePages/components/PurchaseOrderConfirmError";
import PurchaseOrderConfirmed from "../bundles/PurchaseOrderResponsePages/components/PurchaseOrderConfirmed";
import PurchaseOrderUnderReview from "../bundles/PurchaseOrderResponsePages/components/PurchaseOrderUnderReview";
import PurchaseOrderRejected from "../bundles/PurchaseOrderResponsePages/components/PurchaseOrderRejected";
import PurchaseOrderRejectError from "../bundles/PurchaseOrderResponsePages/components/PurchaseOrderRejectError";
import PurchaseOrderCannotBeUserTransitioned from "../bundles/PurchaseOrderResponsePages/components/PurchaseOrderCannotBeUserTransitioned";
import SupplierDirectory from "../components/SupplierDirectory/SupplierDirectory";
import OrganizationPage from "../components/OrganizationPage/OrganizationPage";
import CartSubmittedPage from "../components/CartSubmittedPage";
import BestSellingGreenGoods from "../components/BestSellingGreenGoods";
import { batchedEnhance } from "../../shared/util/registrationHelper";
import HomePage from "../components/HomePage";
import MarketingHomePage from "../components/MarketingHomePage/MarketingHomePage";
import ContactUsPage from "../components/ContactUsPage";
import AboutUsPage from "../components/AboutUsPage/AboutUsPage";
import OurStartPage from "../components/OurStartPage";
import TeamPage from "../components/TeamPage";
import PressPage from "../components/PressPage";
import ProductGroupPage from "../components/ProductGroupPage/ProductGroupPage";
import BranchPage from "../components/BranchPage/BranchPage";
import React from "react";
import InviteRequestPage from "../components/InviteRequestPage";
import LoginPage from "../components/User/LoginPage";
import UnsupportedBrowser from "../components/UserInterventions/UnsupportedBrowser";
import PageWrapper from "../components/layout/PageWrapper";
import TopBar from "../components/Navigation/TopBar";
import FeaturesPage from "../components/FeaturesPage/FeaturesPage";
import SellOnLandscapeHubPage from "../components/SellOnLandscapeHubPage";
import OrganizationBrandsPage from "../components/OrganizationBrandsPage/OrganizationBrandsPage";
import BrandsPage from "../components/brands/index/BrandsPage";
import { HubsheetContextProvider } from "../bundles/Hubsheets/show/contexts/HubsheetContext";
import PasswordResetPage from "../components/User/PasswordResetPage";
import ForgotPasswordPage from "../components/User/ForgotPasswordPage";

const plainComponents = {
  UnsupportedBrowser
};

const enhancedComponent = batchedEnhance({
  AboutUsPage,
  BestSellingGreenGoods,
  BranchPage,
  BrandsPage,
  ContactUsPage,
  FeaturesPage,
  ForgotPasswordPage,
  HomePage,
  HealthCheck,
  InviteRequestPage,
  LoginPage,
  OurStartPage,
  MarketingHomePage,
  PressPage,
  PurchaseOrderAlreadyAcknowledged,
  PurchaseOrderConfirmError,
  PurchaseOrderConfirmed,
  PurchaseOrderRejected,
  PurchaseOrderUnderReview,
  PurchaseOrderRejectError,
  PurchaseOrderCannotBeUserTransitioned,
  OrganizationPage,
  OrganizationBrandsPage,
  PasswordResetPage,
  StandaloneTopBar: dashboardPageProps => (
    <PageWrapper dashboardPageProps={dashboardPageProps}>
      <TopBar showMobileMenu={false} />
    </PageWrapper>
  ),
  SellOnLandscapeHubPage,
  SupplierDirectory,
  TeamPage,
  CartSubmittedPage,
  ProductGroupPage
});

ReactOnRails.register({ ...plainComponents, ...enhancedComponent });

to this, which now only contains MarketingHomePage

import ReactOnRails from "react-on-rails";
import MarketingHomePage from "../components/MarketingHomePage/MarketingHomePage";

ReactOnRails.register(MarketingHomePage);

Let's run webpack and see what happens!

Wahh wahhh, still ~17 MB

image

What is on the marketing home page that could possibly be ~17 MB!? Let's zoom in and see what's being included in this bundle:

image

Weird! Components like Hubsheets, MyOrders, CartIndexPage are included in a bundle only registering MarketingHomePage.

Let's test something. What happens to our bundle if instead of registering MarketingHomePage we register another SSR component like... UnsupportedBrowser!

Now our server-rendering.js file looks like this:

import "./polyfills";
import ReactOnRails from "react-on-rails";
import UnsupportedBrowser from "../components/UserInterventions/UnsupportedBrowser";


ReactOnRails.register(UnsupportedBrowser);

Run webpack annnnnnnd...

image

Woah, server-rendering is only 4.75 MB with most of that being node-modules. You can baaaaarely see app down at the bottom as a sliver that comes out to only 218 KB.

So it looks like for some reason SSRendering MarketinHomePage is pulling in all these other seemingly unrelated components.

SSR means that we are rendering everything to make that page fully functional. So I suppose it makes sense. We have a fully functioning product search bar at the top,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment