Blog Optimization - 1. Main Page Optimization

Table of Contents

Blog Creation Series

TitleLink
1. Basic Settingshttps://witch.work/posts/blog-remake-1
2. HTML Design of the Main Pagehttps://witch.work/posts/blog-remake-2
3. Structure Design of Detailed Post Pageshttps://witch.work/posts/blog-remake-3
4. Enabling Relative Path for Imageshttps://witch.work/posts/blog-remake-4
5. Minor Page Configuration Improvements and Deploymenthttps://witch.work/posts/blog-remake-5
6. Layout Design of Page Elementshttps://witch.work/posts/blog-remake-6
7. Main Page Component Designhttps://witch.work/posts/blog-remake-7
8. Writing List/Content Page Component Designhttps://witch.work/posts/blog-remake-8
9. Automatic Thumbnail Generationhttps://witch.work/posts/blog-remake-9
10. Design Improvements for Fonts, Cards, etc.https://witch.work/posts/blog-remake-10
11. Adding View Counts to Postshttps://witch.work/posts/blog-remake-11
12. Page Themes and Post Search Functionalityhttps://witch.work/posts/blog-remake-12
13. Improving Theme Icons and Thumbnail Layoutshttps://witch.work/posts/blog-remake-13
14. Changing Post Categorization to Tag-basedhttps://witch.work/posts/blog-remake-14
Main Page Computational Optimizationhttps://witch.work/posts/blog-opt-1
Creating Post List Paginationhttps://witch.work/posts/blog-opt-2
Uploading Images to CDN and Creating Placeholdershttps://witch.work/posts/blog-opt-3
Implementing Infinite Scroll on Search Pagehttps://witch.work/posts/blog-opt-4

0. Overview

Many features of the blog have been completed. However, if you enter the deployed page, you can still notice that the page is quite slow. Therefore, I will begin the task of optimizing it to create a page that feels fast to anyone.

This task is named Rapid Witch, taking inspiration from the Rapid Bull Project, which was a server technology innovation project at KakaoTalk aimed at achieving high speeds.

1. Lighthouse Testing

First, I diagnosed my page using Google's well-known open-source Lighthouse, which checks web page quality. After installing the Chrome extension, I was able to easily obtain a diagnostic report.

Lighthouse First Result

Overall, accessibility and SEO were satisfactory (best of all is next-seo!), while performance was lacking and Best Practices were inadequate. PWA readings were also far from ideal. Particularly for performance, despite the other elements being fine, the Total Blocking Time (time taken for a user to interact with the page) was a staggering 1220ms. A good score requires TBT to be below 200ms, making this over six times that mark.

Thus, let’s work hard on performance optimization. Records of the optimizations I think of will be written in order, although the sequence may be somewhat random due to my search for methods.

2. Moving Computation to getStaticProps

On the main page, the Home component continuously calls getSortedPosts. This part does not change significantly after build, so we should move it to getStaticProps. By doing this, it will be called only at build time, potentially increasing build time, but the built page will load quickly.

Let's also ensure that only the necessary information required for post list rendering is passed.

First, remove the part calling getSortedPosts from the Home component in src/pages/index.tsx, and modify the getStaticProps in src/pages/index.tsx as follows.

/* Types used in getStaticProps */
interface CardProps {
  title: string;
  description: string;
  image?: string;
  date: string;
  tags: string[];
  url: string;
}
/* Function to extract only necessary elements from the object */
function propsProperty(post: DocumentTypes) {
  const { title, description, date, tags, url } = post;
  return { title, description, date, tags, url };
}

export const getStaticProps: GetStaticProps = () => {
  const categoryPostMap: { [key: string]: CardProps[] } = {};

  blogCategoryList.forEach((category) => {
    categoryPostMap[category.url] = getSortedPosts()
      .filter((post: DocumentTypes) => {
        return post._raw.flattenedPath.split('/')[0] === category.url.split('/').pop();
      })
      .slice(0, 3)
      .map((post: DocumentTypes) => {
        return propsProperty(post);
      });
  });

  return { props: { categoryPostMap } };
};

Doing this, an object containing {"Category URL":[Top 3 Posts in the Category]} will be passed as props to the page component. Utilize this to display the post list as before.

export default function Home({
  categoryPostMap
}: InferGetStaticPropsType<typeof getStaticProps>) {
  return (
    <main className={styles.pagewrapper}>
      <div className={styles.container}>
        <Profile />
        {/* Create project list */}
        <ProjectList />
        <article>
          {/* Create post list by category */}
          {blogCategoryList.map((category) => {
            const categoryPostList = categoryPostMap[category.url];

            return categoryPostList.length ?
              <Category 
                key={category.title} 
                title={category.title} 
                url={category.url} 
                items={categoryPostList}
              /> : null;
          })}
        </article>
      </div>
    </main>
  );
}

After making this change, the Lighthouse metrics improved significantly. The TBT reduced to about 470ms, nearly halving the time! It is evident how important it is to perform calculations in advance during the build and pass the necessary information.

Lighthouse Second Result

3. Image Optimization - next/image

Currently, the biggest issue with my blog is the slow loading, as noted earlier. Since NextJS supports various image optimizations, let's start with this.

First, enable Next.js image optimization. Update the next.config.js to turn on image optimization that was previously disabled due to Cloudflare. Since I'm only using the Image component from next/image on the main page, this will apply to all images.

const { withContentlayer } = require('next-contentlayer');

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    unoptimized: false,
  },
  reactStrictMode: false,
  swcMinify: false,
};

module.exports = (withContentlayer(nextConfig));

4. Image Optimization - Image Size

Lighthouse suggests properly setting image sizes and links to the sizes section of the next/image tutorial.

The sizes property of the Image tag determines which images to download in the srcset during rendering, and also which source sets to automatically generate in next/image.

If sizes are not set, default sizes or fixed-size images might be automatically generated, which could negatively impact performance if these images are considerably larger than their actual display sizes. To prevent this, we should define sizes.

All images used on the main page pertain to project introductions and are defined in src/components/projectCard/image/index.tsx. Let's specify sizes in the Image component here.

// src/components/projectCard/image/index.tsx
function ProjectImage({ title, image }: { title: string; image: string }) {
  return (
    <div className={styles.container}>
      <Image
        className={styles.image}
        src={image} 
        alt={`${title} project picture`}
        width={300}
        height={300}
        {/* Sizes have been added */}
        sizes='(max-width: 768px) 150px, 300px'
      />
    </div>
  );
}

With this change, TBT dropped to the low 300ms range, occasionally reaching below 200ms. Moreover, I recalled there is one more image on the main page—my profile picture in the profile component. Let's specify sizes for this as well.

// src/components/profile/index.tsx
function Profile() {
  return (
    <article className={styles.profile}>
      <Image 
        className={styles.image} 
        src={blogConfig.picture} 
        alt={`${blogConfig.name}'s profile picture`} 
        width={100}
        height={100}
        sizes='100px'
      />
      <Intro />
    </article>
  );
}

Furthermore, edit the next.config.js to specify images.imageSizes and images.deviceSizes to limit the number of srcset images generated. This will reduce the number of srcsets generated for the images.

This should lead to a reduction in time when generating images upon initial requests. Reference. Therefore, I edited next.config.js as follows to allow only four srcsets.

const { withContentlayer } = require('next-contentlayer');

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    unoptimized: false,
    imageSizes: [64, 384],
    deviceSizes: [768, 1920],
  },
  reactStrictMode: false,
  swcMinify: false,
};

module.exports = (withContentlayer(nextConfig));

Let's strive to bring the TBT below 200ms.

5. Removing Unused JS

Lighthouse also recommends Reduce unused JavaScript. Here's the suggestion:

Reduce unused JavaScript and defer loading scripts until they are required to decrease bytes consumed by network activity. 

In essence, this means to remove unused JS code or delay loading until necessary. Let's identify the problematic JS code.

Unused JS Code

It seems evident that Google Tag Manager is the culprit. Therefore, we will edit the component providing it, GoogleAnalytics.tsx, and change the script loading strategy to lazyOnload. This still allows GA to function properly.

const GoogleAnalytics = () => {
  if (blogConfig.googleAnalyticsId == null) {
    return null;
  }
  return (
    <>
      <Script
        src={`https://www.googletagmanager.com/gtag/js?id=${blogConfig.googleAnalyticsId}`}
        strategy='lazyOnload'
      />
      <Script id='google-analytics' strategy='lazyOnload'>
        {`
          window.dataLayer = window.dataLayer || [];
          function gtag(){window.dataLayer.push(arguments);}
          gtag('js', new Date());

          gtag('config', '${blogConfig.googleAnalyticsId}');
        `}
      </Script>
    </>
  );
};

The time savings here may not be substantial. Note that Google Tag Manager inherently makes network requests, subtly affecting execution time.

Additionally, extensions seem to influence Lighthouse measurements. Testing in incognito mode yielded better scores.

6. Font and Serverless Optimization

Additionally, just in case, I opted for a lighter font used for generating thumbnails with canvas. I used a lightweight version of Noto Sans Korean in Bold style. The previous .otf file was nearly 5MB, while this one is under 300KB.

Furthermore, I changed the default serverless region. I chose the Incheon region, which is closer to the Supabase's Korean region, anticipating faster API routes. (The default region was somewhere in the US.)

It is challenging to pinpoint precisely what has affected what, but after these changes, TBT has stabilized around the 200ms mark, even dipping into the 100ms range occasionally. Naturally, such metrics can fluctuate, occasionally reaching up to 500ms, but the improvement is significant.

Good Lighthouse Result on Main Page

While further reductions are possible, the main page seems sufficiently fast now. However, the detailed post pages and list pages, which load numerous images, still need work, and there are many tasks remaining, including addressing Best Practices to some extent...

7. Reduce Initial Server Response Time

Numerous experiments revealed frequent suggestions to reduce initial server response time. Not seeing this message coincided with improved performance, but it remains a recurrent issue.

Reduce Initial Server Response Time

This aspect actually impacts LCP, definitely affecting the visible parts to users, as this is related to how the page is painted. Optimizations regarding this will likely be explored in subsequent articles.

First, let's address what can be done on the post list/post detail pages.

References

Using Lighthouse: https://velog.io/@dell_mond/Lighthouse-%EC%82%AC%EC%9A%A9%EB%B2%95

Lighthouse Result Metrics: https://medium.com/jung-han/%EB%9D%BC%EC%9D%B4%ED%8A%B8%ED%95%98%EC%9A%B0%EC%8A%A4-%EC%84%B1%EB%8A%A5-%EC%A7%80%ED%91%9C-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0-83df3dc96fb9

Naver's SEO Documentation: https://searchadvisor.naver.com/guide/seo-basic-intro

Image Optimization in Next: https://fe-developers.kakaoent.com/2022/220714-next-image/

Next.js Script Tag: https://nextjs.org/docs/app/api-reference/components/script#strategy

GA functions well even with lazyOnload loading: https://blog.jarrodwatts.com/track-user-behaviour-on-your-website-with-google-analytics-and-nextjs

https://all-dev-kang.tistory.com/entry/Next-%EC%9B%B9%ED%8E%98%EC%9D%B4%EC%A7%80%EC%9D%98-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%EC%9D%84-%ED%95%B4%EB%B3%B4%EC%9E%90-1-featlighthouse

http://theeluwin.github.io/NotoSansKR-Hestia/

https://www.oooooroblog.com/posts/62-optimize-images

https://velog.io/@ooooorobo/Lighthouse%EB%A1%9C-Next.js-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0#%EB%9D%BC%EC%9D%B4%ED%8A%B8%ED%95%98%EC%9A%B0%EC%8A%A4%EA%B0%80-%EB%8F%8C%EC%A7%80-%EC%95%8A%EC%9D%84-%EB%95%8C