There will be those stressful days when you would go crazy thinking about and doing research on how to optimize your website’s performance and speed.
Well, I have gone through that and totally understand the pain so here to share all the findings that helped me and my friends and will definitely help you to improve the page load time of your website.
In SPAs, you would have experienced your main Javascript bundle getting so big(as a single Javascript file is responsible for loading your whole website) that it takes a lot of time for the browser to load, parse, compile and execute your script increasing Time To Interactive of your page.
Well, you are lucky, code splitting is here to save you. It allows you to split your application’s Javascript into smaller chunks allowing you to send minimal code required to load the page visible to the user and the rest can be loaded on demand whenever and wherever required. The best practice is to keep each chunk’s size under 150KB s.t. the app becomes interactive within 5 seconds even on poor networks.
There are two popular ways of implementing code-splitting, route-based and component-based. You really need to be mindful of how you split your app’s code as this will have a major impact on your page speed.
Now, webpack comes up with a lot of ways to implement code-splitting.
Dynamic imports which uses Javascript’s dynamic import api import()
Split code using entry configuration, and
SplitChunksPlugin
Check webpack’s documentation on code-splitting to know more about these. You might wanna check React’s lazy api as well which helps rendering a dynamic import as a regular component.
If you want to do code-splitting in a server rendered app, check Loadable Components.
Tree shaking means eliminating dead code from your script. It came up with ECMAScript 2015 support for static import and export. As these are static imports and exports, we can remove the unused exports from the bundle at compile time.
Remember that tree-shaking happens at compile-time and not run-time in case you have some dynamic imports(we can’t be sure of what part of the code will remain unused).
To enable tree shaking, you can use the sideEffects property of package.json. Set it to false if there are no side-effects in your modules and code in which case webpack will simply remove the unused code from the bundle while compiling.
The sideEffects property accepts an array as well where you can specify paths of the files with side-effects like stylesheets or modules. The paths can be relative, absolute or glob patterns.
In case you wanna specify side-effects per function (e.g inside a function you might have declared variables which are not being used), you can check a webpack optimisation called usedExports . You can also add a comment saying /*#__PURE__*/ on top of the function.
Please note that webpack removes dead-code from the bundle in the production mode, so you would need to set the mode to production in the configuration which will enable minification as well. If you ask me about the development mode then there webpack just adds a comment above the unused code mentioning that piece of code is unused but won’t remove it from the bundle.
Apart from mode you can use --optimize-minimize flag to enable Terser Plugin. A note here, ModuleConcatenationPlugin is being added automatically in the production mode for tree-shaking to work so if you are not going with mode, you need to add the plugin manually.
Once you are done with adding sideEffects status, go ahead, run the build again and check if the dead-code is removed from the generated bundle.
Check webpack’s guide for tree-shaking here for more details.
Lazy-loading means deferring the resources like image, video, etc until they come in the viewport and are actually seen by the user. This reduces unnecessary content-weight, processing required and saves memory making your page performant.
To rephrase, it means when implemented we will be loading only the content visible in viewport and the rest of the content will be loaded when it comes in the viewport or is at a certain distance from the viewport.
You can implement lazy-loading in the following ways.
Intersection Observer API: Most widely used. Cross browser compatibilities can be an issue here as it might not be supported in all the browsers. Check here.
Scroll and resize event handlers: Less performant but good browser compatibility. If you are more concerned about compatibility and can go with a less performant way, you can go with this.
Native loading attribute: With Chrome 76, images and iframe elements are supporting an attribute called loading to lazy or eager load these resources as per requirement. Though not fully supported in all browsers, it’s really easy to implement lazy-loading with this if you want to avoid writing JS or use different libraries and apis to implement lazy-loading.
Please don’t forget to add width and height to <img> element to avoid a reflow when images are downloaded on lazy-load. Next point will give you more insight into this.
Lazy-loading of the videos depends on the user requirement and needs a little more attention than the rest. I am considering that you are using <video> element to include videos on your pages which is the case most of the times.
In case you have disabled autoplay for your videos, you need not preload the video content until the user plays the video. <video>element comes with a preload attribute which gives the browser a hint for what should be the video content behavior for the targeted user. You should set it’s value as none to avoid preloading any video data and provide a poster attribute to add a placeholder.
Remember that this doesn’t force the browser to follow what you define instead it acts as a hint to the browser.
The default for preload used to be auto in Chrome, now from Chrome 64 onwards, it’s metadata. Like this different browsers have different defaults under different conditions e.g. in Data saver mode it’s set to none.
Now preload is not the only way to defer video content loading, check here.Check link preload here to preload just the first segment which would be good to follow if you have autoplay enabled.
Note: Autoplay takes priority over preload.
Important: Always define the width and height of an image to make your page performant and to have a smooth user-experience.
This avoids the layout shifts(jank problems) by preserving proper placeholder space for the images on your page while they are getting downloaded.
Now, in the era of responsive web design, when you would want your images to be responsive you start resizing your images using css, say by adding
in your stylesheet and our solution to prevent the jank no longer worked here i.e. we again start to see layout shifts and thus multiple repaints.
Recently, Mozilla came up with the idea to use the width and height attributes in the image element to compute its aspect-ratio thus calculating the image dimensions before it is fully loaded. This prevents unnecessary re-layouts when the image loads and improves performance a lot especially on super slow network connections.
This support has recently been shipped with Firefox 71 and Chrome 79 in December 2019. The aspect-ratio CSS property is still in experimental stage to ease this further extending the support to all the elements.
Check this amazing video for a more detailed explanation.
Animated GIFs can be utterly huge scaling up to several megabytes. Luckily we can convert our GIFs to thin fast video files and use the <video> element to achieve GIF looking behaviour by enabling autoplay and making them loop silently which is as easy as
This not only means chopping off megabytes from your page but reducing CPU time as well(videos use less CPU time than GIFs) which is an important aspect to improve page-load time beyond reducing files sizes.
You should avoid including scripts using document.write as they can be parser blocking. Also if document.write is run once the document is loaded, it will clear the document again and write to it thus affecting your performance.
So, it’s not considered a good practice to use it and continuing from Chrome 55, any script included using document.write won’t be executed(if on a 2G connection along with some other conditions).
All those third-party scripts that you are planning to import might not have the support to be loaded asynchronously which as a result will block the further execution of your page until this script is fully downloaded and executed and in case if this script is using other scripts, it might end up being lot of round network trips drastically slowing down your page.
If you are including some third party code-snippets, don’t forget to load them asynchronously using async/defer(please check what is better for you to use). Use appendChild() instead of document.write if you have to choose.
Also please make sure that the third-party scripts you are using have the support to be loaded asynchronously or else find an alternative which does.
Believe me this helps as we always end up having a big chunk of unused CSS without realising.
Remove all the repeated css properties and the unnecessary overrides. Use css-modules to locally scope the CSS selectors. This will let you have a separate css module for each component. Thus when you implement say, tree-shaking, code-splitting, etc, the css module will be shaken off as well or will get in a separate bundle along with its scoped component module.
Doing this will eliminate the unused CSS improving page-load time at a greater extent.
A point to keep in mind is that CSS is treated as a render blocking resource which means the browser won’t render any processed content until CSS Object Model is constructed. Use media types and media queries to help browser mark some CSS resources as non-render blocking.
Reduce the complexity of the CSS selectors you use. Sometimes you end up using selectors like this:
In order for the browser to figure out which element to apply these styles to it would have to ask a question like “is this an element with a class of headline which has a parent who is the nth child minus one element with a class of list”. Figuring this out can take a lot of time depending on the browser, so you should instead go with the following:
Please don’t mind the class name, the point being that now it will be a lot easier for the browser to target and compute styles for an element with a class of custom-list-headline. BEM optimise this behaviour following class-centric methodology and there are other efficient ways to approach your selectors as well.
Reflow is a user-blocking operation to recalculate the positions and dimensions of elements in the document thus re-rendering either a part or the whole document.
You will be amazed to know how easily we end up having a lot of reflows in our documents e.g. just by changing css styles or class of an element, pseudo classes like :hover , animations, adding/removing/updating elements in DOM, etc.
This hits performance as updating a single element can affect it’s children, ancestors and siblings resulting in more time being spent to perform the reflow. That’s why it is always recommended to reduce the DOM depth.
There’s a lot more to reflow and if you are interested, I found some good info on it here by Lindsey Simon and here by Charis Theodoulou. There’s a list of apis mentioned here which when called in JS will force a reflow.
Reduce paint areas
Paint is often the longest running tasks of all in the pipeline to render your page so we should try to reduce paint areas and simplify it’s complexity. Check layer promotion here to reduce paint areas.
Always be mindful while choosing the slider libraries for your website as they can really slow down your page e.g.react-slick is known to cause expensive forced reflow of your layouts.
I would recommend using Glide.js as it’s light-weight, fast and have not caused any performance issue for us so far.
Use a bundle analyser like webpack-bundle-analyzer to visually analyse your bundles and remove the packages no longer required. Check the size of the packages used and use lightweight alternatives if available.
Run npm dedupe in your app to delete the duplicates of npm packages(different versions of the same package) and optimise your package tree.
Also, move to the latest versions of packages as they might be more efficient having followed some performance optimisations themselves.
Import only the modules you need rather than importing the whole libraries. For example:
Follow
instead of
In the first case you are importing just the module(~2kB) as opposed to the second case where you ended up importing the whole library(~25kB).
You should follow the habit of writing smaller functions limiting yourself to not exceed a certain number of lines per function.
Along with it’s other benefits if you implement tree-shaking, functions written this way will help eliminate every tiny piece of unused code.
Cache your code to minimise network trips, you can implement HTTP caching, service-worker caching, if using webpack then check filename hashing, CDNs, etc.
To start with you can always go with HTTP caching as it’s easy to implement though won’t give much flexibility but it’s effective and supported in all browsers.
Caching is a vast field in itself and I won’t be able to tell you all about it here but surely will get back to you on this in future including details on several other types of caching as well like database caching.
If you are showing ads on your pages, you need to be really careful for how you are loading them.
Always load your ads asynchronously or after your page has been loaded as they can block your main thread for a long time affecting the performance of your page.
Last but not the least check this blog where I have explained how a little negligence while using target attribute in anchor tags can affect the performance and security of your app.
I have tried to include everything that I found during my research and what helped me in optimising the page speed. I hope you find it useful too. You can always dive deeper into these topics and learn more about their implementations as now you know what to look for.
If you have some more suggestions for how to optimise the page speed and what helped you improve your page load time, I will be really happy to know about it. Please write in the response below or you can ping me directly.
At Quintype, we are always making efforts to optimize web page performance.
Reposting story of one of our senior full stack developers at Quintype - https://levelup.gitconnected.com/website-performance-optimization-cd1647498274