--Disclaimer--
Please excuse me if I make any conceptual mistakes. I'm a product guy but crazy about performance so I got myself into playing with code every now and then to test the performance of my app.
In this ticket, I will use my product as a Benchmark and I'll be sharing essential information to see where the problem is.
I don't know if this problem is specific to my product or is a deficiency of how the framework works.
Ok, let's get started!
My website is called Mahkamati It's a Q2A for legal matters.
On Web dev, it floats around 64 in performance which is not where I want it to be.
On the Backend, we are using AdonisJS Proxied by Nginx and cached for 24 hours so the backend response is lightning fast 鈿★笍
But the problem we are facing right now, is the FE speed.
On the FE, we use Nuxt JS+Vuetify + some plugins (Rudderstack, that injects FB pixel and others that are not that heavy)
As you can see from the Web dev results, our FID is high and the Total blocking time is high.
The profiler looks like this x4 slowdown on a MAC (1.4 GHz Quad-Core Intel Core i5):
You can download the profiler from here:
Profile-20200710T174938.txt
As you can see, the main thread doesn't take a rest until later, so the user can not interact with the app.
The direction where I wanted to go is explained by Google's Web.dev
Their idea is the following:
If blocking time is anything above 50ms then instead of having a task that executes a JS for 2 secs, where TBT (Total blocking time) = 2000 - 50 = 1950 ms, you can split that file into 40 files where each executes at 50ms and your total blocking time will be from 1950 ms to 0ms!
It's easier said than done, because lower size doesn't necessarily mean lower execution time.
But, it's theoretically a good direction to take.
So what causes this long JS task?
If you look at the profiler, you can see 2 big JS executions, the second one is a big translation file (That's on me) but the first one is the app.js which is the entry point of the app, which includes all the modules and plugins that my app is using.
Now my idea or question is, can we break down app.js into multiple tasks that execute around 50ms, how can we do it?
Is it Nuxt? Webpack? Both?
I would love to hear your thoughts.
Thank you.
Plus one here! We have exactly the same problem!
@sshuichi did you found any solution?
Facing the same issue with my website.
https://www.rummytime.com/
@ricardobt, no. I'm hoping things will get better with Webpack5 and Vue3.
I'm guessing that this first blocking task is vue hydration process. With this plugin: https://github.com/maoberlehner/vue-lazy-hydration you can lower it down significantly (if properly used - read the specs).
Later on remember to render only required app shell
and dynamically load other components (webpack will take care to split them into separate files).
I second @voltane idea to leverage lazy-hydration, reducing JS _execution time_ and Chunk splitting is mainly wepack's task so nothing much to do (hopefully would be improved by WP5)
One important tip worth mentioning is that nuxt plugins are usually main reason of blocking render/hydration since nuxt awaits on them before start rendering on client-side and also their dependencies will be added to main (or vendors) chunk which both are necessary to bootstrap too.
(NOTE: All examples below are for .client
plugins)
Example 1: Defer plugins with a background task
Bad practice:
export default async function(ctx, inject) {
await ...
}
Doing task in parallel to render:
async function task(ctx) {}
export default function(ctx, inject) {
task(ctx).catch(console.error)
}
Using onNuxtReady
to defer task after app is mounted:
async function task(ctx) {}
export default function(ctx, inject) {
window.onNuxtReady(() => task(ctx).catch(console.error))
}
Example 2: Lazy importing dependencies
Bad practice:
import bigDep from 'big-dep'
export default function (ctx, inject) {}
Create a chunk to reduce execution time (and also preload in parallel) but still blocks render:
export default async function (ctx, inject) {
const bigDep = await import('big-dep' /* webpackChunkName: 'big-dep' */)
}
Do logic in background:
async function task(ctx) {
const bigDep = await import('big-dep' /* webpackChunkName: 'big-dep' */)
}
export default function (ctx, inject) {
task(ctx).catch(console.error)
}
Example 3: Lazy import by usage:
Bad practice: (usage: this.$util.foo()
)
import getbigDep from 'big-dep'
export default function (ctx, inject) {
inject('util', {
async foo() {
// some logic depending on bigDep
}
})
}
Lazy import by usage: (same usage)
const getbigDep = () => import('big-dep' /* webpackChunkName: 'big-dep' */)
const createUtils = ctx => ({
async foo() {
const bigDep = await getbigDep()
// some logic depending on bigDep
}
})
export default function (ctx, inject) {
inject('util', createUtils(ctx))
}
Lazy import entire utils if utils themselfe are big: (Usage this.$utils().then(utils => ...)
)
// plugins/foo.utils.js
// we can either directly import or utilize lazy import too
export default (ctx) {
return { ... }
}
// plugins/foo.client.js
export default (ctx, inject) {
inject(utils => import('./foo.utils').then(createUtils => createUtils(ctx))
}
Most helpful comment
I second @voltane idea to leverage lazy-hydration, reducing JS _execution time_ and Chunk splitting is mainly wepack's task so nothing much to do (hopefully would be improved by WP5)
One important tip worth mentioning is that nuxt plugins are usually main reason of blocking render/hydration since nuxt awaits on them before start rendering on client-side and also their dependencies will be added to main (or vendors) chunk which both are necessary to bootstrap too.
(NOTE: All examples below are for
.client
plugins)Example 1: Defer plugins with a background task
Bad practice:
Doing task in parallel to render:
Using
onNuxtReady
to defer task after app is mounted:Example 2: Lazy importing dependencies
Bad practice:
Create a chunk to reduce execution time (and also preload in parallel) but still blocks render:
Do logic in background:
Example 3: Lazy import by usage:
Bad practice: (usage:
this.$util.foo()
)Lazy import by usage: (same usage)
Lazy import entire utils if utils themselfe are big: (Usage
this.$utils().then(utils => ...)
)