cover

A Little Optimization on the Loading Volume of Blog Web Pages

sorcererxwβ€’

Recently, I found that the size of the blog webpage is a bit too large, requiring over 200KB of data to be downloaded for the first load.

Page                                               Size     First Load JS
β”Œ ● / (364 ms)                                     4.39 kB         237 kB
β”œ   /_app                                          0 B             224 kB
β”œ β—‹ /404                                           1.2 kB          234 kB
β”œ β—‹ /500                                           1.2 kB          234 kB
β”œ ● /about (1698 ms)                               366 B           283 kB
β”œ ● /articles/[id] (147788 ms)                     4.23 kB         287 kB
β”œ   β”œ /articles/jike-golang (19000 ms)
β”œ   β”œ /articles/build-blog-with-notion (10896 ms)
β”œ   β”œ /articles/jikeview (10152 ms)
β”œ   β”œ /articles/go-echo-error-handing (9725 ms)
β”œ   β”œ /articles/vercel-go-microservice (9300 ms)
β”œ   β”œ /articles/go-string-interning (6804 ms)
β”œ   β”œ /articles/go-comparable-type (6178 ms)
β”œ   β”” [+23 more paths] (avg 3293 ms)
β”œ Ξ» /sitemap.xml                                   274 B           225 kB
β”” ● /tags/[tag] (6682 ms)                          4.42 kB         237 kB
    β”œ /tags/engineering (723 ms)
    β”œ /tags/android (712 ms)
    β”œ /tags/node.js (698 ms)
    β”œ /tags/hack (688 ms)
    β”œ /tags/network (681 ms)
    β”œ /tags/go (664 ms)
    β”œ /tags/product (662 ms)
    β”” [+5 more paths] (avg 371 ms)
+ First Load JS shared by all                      224 kB
  β”œ chunks/framework-fae18336d1dfe3fb.js           42 kB
  β”œ chunks/main-91270026f6d0b490.js                28.4 kB
  β”œ chunks/pages/_app-9742e6e7f9139000.js          148 kB
  β”œ chunks/webpack-7c44f1ec48856c43.js             6.11 kB
  β”” css/c4ae7c55caaef550.css                       1.5 kB

Through @next/bundle-analyzer, you can clearly see which packages on the Client Side occupy a large amount of space.

Following the above analysis, I will optimize each package one by one.

Syntax Highlighter

First and foremost is the Syntax Highlighter, which is used to highlight code block syntax. From the analysis chart above, we can see that the code related to highlight (highlight.js, prism.js) almost occupies half of the territory. Therefore, we will start optimizing from it.

Previously, for simplicity, the page directly used https://github.com/react-syntax-highlighter/react-syntax-highlighter. It requires the introduction of theme and parser schema at runtime, which will introduce a considerable overhead.

So, is it possible to render the code block directly on the server side, making it zero-dependency for the front end? Here, I chose https://github.com/shikijs/shiki, which is lighter and more friendly for node environment rendering. It can render the code into html directly on the server side based on the specified language and theme, and the front end only needs to insert it directly into the page.

Currently, in order to reduce the complexity of SSG, the code blocks are directly rendered as plain text during SSG, and then the frontend dynamically fetches the highlighting results of the current code block from the server at runtime.

lodash

From the Analyzer, it can also be seen that lodash occupies a considerable amount of space. Theoretically, I only used some of the lodash functions, so why didn't it tree shake but instead packaged the entire library? After researching, it turns out that tree shaking only works for esModule, but lodash is not, so it has to be fully packaged.

To solve the above problem, lodash offers a lodash-es version. After using it, it was found that although tree shaking was effective, lodash-es does not provide the chain function in lodash. This means that complex operations cannot use chain operations, but need to wrap logic layer by layer:

import _ from 'lodash'

_.chain(data)
 .filter(it=>it.ok)
 .map(it=>it.something)
 .flatten()
 .value()
import _ from 'lodash-es'

_.flatten(
	_.map(
	  _.filter(
			data, 
			it=>it.ok,
		), 
		it=>it.something,
	)
)

For complex data operation logic, the onion-ring-like operations are troublesome and not clear enough, and they cannot reflect the advantages of lodash. However, it is understandable, after all, chain itself integrates all operations and cannot remove some member functions.

Since lodash can't meet my needs very well, I decided to stop using lodash and operate data based on native map-reduce. Reduce itself is powerful enough, but its main drawback is that it lacks code reusability. For example, the flatten operation needs to be implemented wherever it is used. Based on the snippet provided by https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore, an operation can be completed quickly.

Chakra ❌ Tailwind βœ…

Initially, Chakra UI was used as the UI component in the webpage, inheriting the flexibility of tailwind while avoiding overly large className. However, upon analyzing the Bundle, it was found that Chakra itself still occupied a considerable amount of volume.

Reduce theme bundle size Β· Issue #4975 Β· chakra-ui/chakra-ui
Description Hi, I'm @hiroppy from webpack team. First, thank you for creating a wonderful library and I am satisfied with it other than one thing which is the huge bundle size. I want to discuss yo...
https://github.com/chakra-ui/chakra-ui/issues/4975

The solution is that Chakra splits each component into separate packages, allowing developers to import as needed.

import sliderTheme from "@chakra-ui/theme/slider"
import accordionTheme from "@chakra-ui/theme/accordion"
import { extendTheme } from "@chakra-ui/react"

However, I still thought it was not lightweight enough, so I decided to give up Chakra UI and switch to tailwind instead. Tailwind compiles CSS files during the compilation phase and does not need to introduce additional JS code during runtime, which is quite lightweight. Since Chakra UI and tailwind have almost the same style naming, the switch is very convenient.

NEXT_DATA

To enhance the loading data, the webpage extensively utilizes the features of Next.js SSG, which renders the page during the compilation phase, and then directly issues the webpage from the CDN. However, in order for React to hydrate successfully, Next.js will place the server side props in the NEXT_DATA for distribution.

Even if only a small part of the data is actually used, it will not be tree shaken. You can imagine that if too much data is sent without control, it will inevitably lead to the expansion of the HTML size. Therefore, when populating data on the server side, it is necessary to remove as much unnecessary content from the page as possible, in order to minimize the final data included.

Why does `__NEXT_DATA__` exists? Β· vercel/next.js Β· Discussion #15117
Can someone explain why the following element exists on my page: <script id="__NEXT_DATA__" type="application/json"> { "props":{ "pageProps":{} }, "page":"/", "query":{}, "buildId":"mgml30LY3PGj-2I...
https://github.com/vercel/next.js/discussions/15117

Of course, if there are no requirements for SEO and loading speed, converting components to asynchronous loading is also a good solution. Currently, there are two solutions:

  • One approach is to use SWR to fetch server-side data and then render the corresponding components on the front end. However, this solution may introduce some additional component dependencies, leading to an increase in the JS load of the webpage.
  • Another solution is the Server Component introduced in React18, which can render the corresponding components directly on the server and send them directly to the front end. This solution seems more elegant than the one above.

Currently, I have implemented the first solution on some components. Once Next.js has better support for React18, I will try out the second solution.

Summary

After a series of operations, the size of the webpage has been significantly reduced, with the JS size of the first page load dropping from 200+KB to 70+KB. In summary, try not to introduce too many components at the client side runtime, and complete as much work as possible during the compilation period and on the server side.