왜 코드 스플리팅을 하는 게 좋을까?

1. 빠른 렌더링으로 사용자의 이탈을 막을 수 있다

2. 사용자가 안들어가는 페이지의 네트워크 요청량을 줄일 수 있다

3. 네트워크 요청 횟수는 늘어나지만 HTTP/2의 영향으로 여러 요청을 동시에 처리 가능해지면서

여러 작은 청크로 나누는 것이 대체로 더 효율적이게 되었다

4. 필요한 기능만 로드되어 메모리 사용 최적화

 

한국처럼 네트워크가 좋은 국가에서는 느린 4G(LTE) 환경이 현실적 기준이라고 한다.

개발자 모드의 네트워크에서 느린 4G로 쓰로틀링을 걸고 성능 탭에서 측정을 하였다

 

(수정)

측정값중 FCP는 loading spinner가 나올 때도 측정이 되어서 유의미한 결과로 볼 수 없다고 한다.
그래서 LCP( 사용자가 중요한 컨텐츠를 빠르게 볼 수 있는지 평가)
TTI(페이지와 상호작용이 가능한 시점)
위 두가지를 주로 사용한다고 한다.

다음에는 TTI도 테스트 대상에 넣어야겠다.

FCP (First Contentful Paint)

  • FCP는 페이지가 로드를 시작한 시점부터 의미있는 컨텐츠가 처음 렌더링 되는 시점까지의 시간을 측정하는 지표이다
    FCP
  • 의미있는 컨텐츠랑 텍스트, 이미지, SVG, canvas등의 요소가 해당되며
    "현재 이 웹 페이지가 로드되고 있구나"라는 것을 인지하는 시점이라 할 수 있다

LCP (Largest Contentful Paint)

  • LCP는 사용자의 뷰포트에서 가장 큰 이미지 혹은 텍스트 블록이 렌더링 되는 시간을 측정하는 지표이다

LCP

  • 이는 뷰포트내에서 가장 큰 요소가 사용자에게 중요한 요소일것이라고 가정했기 때문이며 대상이 되는 요소들은, IMG, SVG 내부의 이미지, 비디오, 텍스트, 배경이미지를 url 함수로 가져오는 요소들이 해당된다
  • 요소의 크기를 측장할 때는 CSS와 관련된 부분(패딩, 마진 등)은 측정값에서 제외하며 이미지의 원본 크기와 렌더링 된 크기가 다를 경우 더 작은 값을 기준으로 한다

목표 값

  • 현실적으로 느린 4G(LTE)환경에서 목표 값은
  • FCP 목표값: 3초 이하
  • LCP 목표값: 4초 이하
    라고 한다

확장 프로그램이 없는 시크릿 모드에서 최적화 전의 값

  • FCP: 7.17초
  • LCP: 7.19초

뭔가 값이 이상해서 생각해보았는데

지금 vscode에서 npm run dev로 돌리고 중인데 지장이 있을거라고 의심되었다

그래서 claude에게 질문하였고

npm run build -> npm run preview를 해야 제대로 된 값이 나온다는 답변을 받았다

확장 프로그램이 없는 시크릿 모드에서 최적화 전의 값

  • FCP: 2.26초
  • LCP: 2.27초

최적화 후의 값

  • FCP: 2.27초
  • LCP: 2.29초

차이가 별로 안나고 정상적인 작동을 할 경우 lazy를 한 라우트일 경우 다시 요청이 가야하는데

전혀 변화가 없었다

그래서 원인을 찾았는데

action과 loader를 정적인 상태로 놓았다는 점

vite.config.ts에서 manualChunks로 라우트별 청크 분리를 안했다는 점이다

import {StrictMode, Suspense, lazy} from 'react';
import {createRoot} from 'react-dom/client';
import {
  createBrowserRouter,
  Outlet,
  RouterProvider,
  ScrollRestoration,
} from 'react-router-dom';
import './index.css';
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
import ErrorLayout from './components/ErrorLayout.tsx';
import {ReactQueryDevtools} from '@tanstack/react-query-devtools';
import {ErrorBoundary} from './components/ErrorBoundary.tsx';
import RootPage, {loader as rootLoader} from './routes/root.tsx';
import IndexPage from './routes/home.tsx';
import ProtectedRoute from './components/ProtectedRoute.tsx';

// Lazy load components
const LoginPage = lazy(() => import('./routes/login.tsx'));
const RegisterPage = lazy(() => import('./routes/register.tsx'));
const SettingsPage = lazy(() => import('./routes/settings.tsx'));
const EditorPage = lazy(() => import('./routes/editor.tsx'));
const ArticlePage = lazy(() => import('./routes/article.tsx'));
const ProfilePage = lazy(() => import('./routes/profile.tsx'));

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60 * 1000,
      retry: 1,
    },
  },
});

const LoadingSpinner = () => (
  <div className="flex justify-center items-center min-h-[200px]">
    <div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-green-500"></div>
  </div>
);

const router = createBrowserRouter([
  {
    path: '/',
    errorElement: <ErrorLayout />,
    element: (
      <Suspense fallback={<LoadingSpinner />}>
        <RootPage />
        <ScrollRestoration
          getKey={(location) => {
            return location.key;
          }}
        />
      </Suspense>
    ),
    loader: rootLoader(queryClient),
    children: [
      {
        index: true,
        element: <IndexPage />,
      },
      {
        path: '/login',
        element: <LoginPage />,
        action: async (args) => {
          const {action} = await import('./routes/login.tsx');
          return action(queryClient)(args);
        },
      },
      {
        path: '/register',
        element: <RegisterPage />,
        action: async (args) => {
          const {action} = await import('./routes/register.tsx');
          return action(queryClient)(args);
        },
      },
      {
        path: '/settings',
        element: (
          <ProtectedRoute>
            <SettingsPage />
          </ProtectedRoute>
        ),
      },
      {
        path: '/editor',
        element: (
          <ProtectedRoute>
            <Outlet />
          </ProtectedRoute>
        ),
        children: [
          {
            index: true,
            element: <EditorPage />,
            action: async (args) => {
              const {action} = await import('./routes/editor.tsx');
              return action(queryClient)(args);
            },
          },
          {
            path: ':slug',
            element: <EditorPage />,
            loader: async (args) => {
              const {loader} = await import('./routes/editor.tsx');
              return loader(queryClient)(args);
            },
            action: async (args) => {
              const {action} = await import('./routes/editor.tsx');
              return action(queryClient)(args);
            },
          },
        ],
      },
      {
        path: '/article/:slug',
        element: <ArticlePage />,
        loader: async (args) => {
          const {loader} = await import('./routes/article.tsx');
          return loader(queryClient)(args);
        },
      },
      {
        path: '/deleteArticle/:slug',
        action: async (args) => {
          const {action} = await import('./routes/deleteArticle');
          return action(queryClient)(args);
        },
      },
      {
        path: '/profile/:username/*',
        element: <ProfilePage />,
      },
    ],
  },
]);

const container = document.getElementById('root');
if (container) {
  const root = createRoot(container);
  root.render(
    <StrictMode>
      <ErrorBoundary>
        <QueryClientProvider client={queryClient}>
          <RouterProvider router={router} />
          {process.env.NODE_ENV === 'development' && (
            <ReactQueryDevtools buttonPosition="bottom-right" />
          )}
        </QueryClientProvider>
      </ErrorBoundary>
    </StrictMode>,
  );
}
import {defineConfig, loadEnv} from 'vite';
import react from '@vitejs/plugin-react';

// https://vitejs.dev/config/
export default defineConfig(({mode}) => {
  const env = loadEnv(mode, process.cwd());
  // console.log(env.VITE_API_URL);
  if (mode === 'development') {
    return {
      plugins: [react()],
      server: {
        proxy: {
          '/api': {
            target: env.VITE_API_URL,
            changeOrigin: true, // CORS 우회
            rewrite: (path) => path.replace(/^\/api/, ''), // 경로 수정
            secure: false, // HTTPS 인증서 문제 우회
          },
        },
      },
      build: {
        rollupOptions: {
          output: {
            manualChunks: (id) => {
              // 디버깅을 위한 로그
              console.log('Processing:', id);

              // node_modules 분리
              if (id.includes('node_modules')) {
                return 'vendor';
              }

              // 라우트별 청크 분리
              if (id.includes('/routes/')) {
                const routeName = id.split('/routes/')[1].split('.')[0];
                // action과 컴포넌트를 같은 청크로 묶기
                if (
                  [
                    'login',
                    'register',
                    'editor',
                    'article',
                    'profile',
                    'settings',
                  ].includes(routeName)
                ) {
                  return `route-${routeName}`;
                }
              }

              // 그 외의 경우 기본 청크로
              return 'main';
            },
          },
        },
      },
    };
  }
  return {
    plugins: [react()],
  };
});

모든 부분을 적용하고 잰 결과

  • FCP: 1.58초
  • LCP: 1.60초

약 0.69초 정도의 성능 향상이 있었다.

지금 프로젝트가 크지 않아서 큰 차이는 없지만

유의미한 변화라고 생각된다

출처

https://velog.io/@sisofiy626/Web-%EC%9B%B9-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%84%B1%EB%8A%A5-%EC%B8%A1%EC%A0%95-%EC%A7%80%ED%91%9C

+ Recent posts