Initial page load with React & Vite takes a very long time
Asked Answered
D

2

8

Vite actually runs nearly instantly:

vite

However, when I try visiting the url, I have a blank page that loads for a while, and after that, everytime I try loading a page for the first time, a similar waiting time ensues, except the already loaded components, such as the navbar and header, don't need to rerender:

image

Here's my package.json:

{
    "name": "myapp",
    "version": "6.1.0",
    "author": "redacted",
    "homepage": "redacted",
    "private": true,
    "scripts": {
        "dev": "vite --port 3000",
        "build": "tsc && vite build",
        "preview": "vite preview",
        "lint": "eslint . --ext .ts,.tsx"
    },
    "browserslist": {
        "production": [
            ">0.2%",
            "not dead",
            "not op_mini all"
        ],
        "development": [
            "last 1 chrome version",
            "last 1 firefox version",
            "last 1 safari version"
        ]
    },
    "dependencies": {
        "@auth0/auth0-spa-js": "2.0.3",
        "@aws-amplify/auth": "5.1.8",
        "@emotion/cache": "11.10.5",
        "@emotion/react": "^11.10.5",
        "@emotion/server": "11.10.0",
        "@emotion/styled": "^11.10.5",
        "@fullcalendar/core": "6.1.4",
        "@fullcalendar/daygrid": "6.1.4",
        "@fullcalendar/interaction": "6.1.4",
        "@fullcalendar/list": "6.1.4",
        "@fullcalendar/react": "6.1.4",
        "@fullcalendar/timegrid": "6.1.4",
        "@fullcalendar/timeline": "6.1.4",
        "@loadable/component": "^5.15.3",
        "@mui/icons-material": "5.11.9",
        "@mui/lab": "5.0.0-alpha.120",
        "@mui/material": "^5.11.9",
        "@mui/system": "5.11.9",
        "@mui/x-date-pickers": "5.0.18",
        "@react-pdf/renderer": "3.1.4",
        "@reduxjs/toolkit": "1.8.5",
        "@syncfusion/ej2": "^20.4.54",
        "@syncfusion/ej2-react-grids": "^20.4.54",
        "@untitled-ui/icons-react": "0.1.0",
        "apexcharts": "3.37.0",
        "aws-amplify": "5.0.14",
        "axios": "^1.3.4",
        "date-fns": "2.29.3",
        "draft-js": "0.11.7",
        "firebase": "9.17.1",
        "formik": "2.2.9",
        "framer-motion": "^10.6.0",
        "fuse": "^0.4.0",
        "fuse.js": "^6.6.2",
        "gray-matter": "4.0.3",
        "i18next": "22.4.9",
        "lodash.debounce": "4.0.8",
        "lodash.isequal": "4.5.0",
        "mapbox-gl": "2.12.1",
        "mui-one-time-password-input": "1.0.4",
        "nprogress": "0.2.0",
        "numeral": "2.0.6",
        "prop-types": "15.8.1",
        "react": "18.2.0",
        "react-apexcharts": "1.4.0",
        "react-beautiful-dnd": "13.1.1",
        "react-dom": "18.2.0",
        "react-draft-wysiwyg": "1.15.0",
        "react-dropzone": "14.2.3",
        "react-helmet-async": "1.3.0",
        "react-hot-toast": "2.4.0",
        "react-i18next": "12.1.5",
        "react-map-gl": "7.0.21",
        "react-markdown": "8.0.5",
        "react-quill": "2.0.0",
        "react-redux": "8.0.5",
        "react-router": "6.8.1",
        "react-router-dom": "6.8.1",
        "react-slick": "0.29.0",
        "react-syntax-highlighter": "15.5.0",
        "redux": "4.2.1",
        "redux-devtools-extension": "2.13.9",
        "redux-thunk": "2.4.2",
        "simplebar-react": "3.2.1",
        "styled-components": "^5.3.9",
        "stylis": "4.1.3",
        "stylis-plugin-rtl": "2.0.2",
        "yup": "1.0.0"
    },
    "devDependencies": {
        "@svgr/webpack": "6.5.1",
        "@types/lodash": "4.14.191",
        "@types/node": "18.13.0",
        "@types/nprogress": "0.2.0",
        "@types/numeral": "2.0.2",
        "@types/react": "18.0.28",
        "@types/react-beautiful-dnd": "13.1.3",
        "@types/react-dom": "18.0.11",
        "@types/react-draft-wysiwyg": "1.13.4",
        "@types/react-redux": "7.1.25",
        "@types/react-slick": "0.23.10",
        "@types/styled-components": "^5.1.26",
        "@typescript-eslint/eslint-plugin": "^5.54.0",
        "@vitejs/plugin-react": "^3.1.0",
        "autoprefixer": "^10.4.13",
        "eslint": "8.34.0",
        "eslint-config-prettier": "^8.6.0",
        "eslint-import-resolver-typescript": "^3.5.3",
        "eslint-plugin-import": "^2.27.5",
        "eslint-plugin-jsx-a11y": "^6.7.1",
        "eslint-plugin-prettier": "^4.2.1",
        "eslint-plugin-react": "^7.32.2",
        "eslint-plugin-react-hooks": "^4.6.0",
        "postcss": "^8.4.21",
        "postcss-cli": "^10.1.0",
        "prettier": "2.8.4",
        "tailwindcss": "^3.2.7",
        "typescript": "4.9.5",
        "vite": "^4.1.4",
        "vite-plugin-eslint": "^1.8.1",
        "vite-plugin-svgr": "^2.4.0",
        "vite-tsconfig-paths": "^4.0.5"
    }
}

My vite.config.ts:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsconfigPaths from 'vite-tsconfig-paths';
import svgrPlugin from 'vite-plugin-svgr';
import eslint from 'vite-plugin-eslint';

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [
        react(),
        eslint({
            emitWarning: true,
            emitError: true,
            failOnError: true,
        }),
        viteTsconfigPaths(),
        svgrPlugin(),
    ],
});

If it helps, I'm also using Tailwind with the standard config file.

Here's the redacted Gist with the HAR generated by Chrome.

This is immensely slowing down development. The project isn't big enough to warrant this much of a slowdown.

Denning answered 27/3, 2023 at 2:23 Comment(9)
This might help you out: betterprogramming.pub/…Rubenstein
In development Vite won't be bundling anything, and as you appear to be running the dev server using HTTP instead of HTTPS (which would allow for HTTP/2 if you aren't using the inbuilt proxy) it will most likely be using HTTP/1.1 and only grabbing around 6 files at a time. If you have a large solution or a lot of 3rd party packages this could be contributing to your start up time. Anything we could say here would be a guess at best, a HAR would go a long way in pin pointing the issue as we would be able to see if the hold up is backend or frontend.Catatonia
@JacobSmit I was planning on doing that, but I'm unsure over how I would share the HAR. It's quite bigDenning
Actually, I used GitHub Gist. Check the updated postDenning
So from what I can see you have 2 issues A) you are pumping out over 500 requests to the dev server at 6 concurrent requests at a time. B) files being served by the dev-server take an exceptionally long time to be received, even your initial HTML page takes 2+ seconds to load. Settings HTTPS on there server config will help with issue A, but most likely will have no impact on issue B. What kind of hardware (virtual or physical) are you working with? Is your HDD on it's way out? Do you have some other recurring task or file writing that is interacting poorly with the dev server watch?Catatonia
You might try updating to Vite 4.3+ as well, they seem to have been working on this issue and have some made some performance improvements: github.com/vitejs/vite/issues/12363Catatonia
@JacobSmit Using HTTPS didn't help much. This issue is also on other developers' computers, and I have a brand new SSD. The only thing I can think of that could be interacting with the dev watch is ESLint or Prettier or some Vite plugin (I have eslint, viteTsConfigPaths, svgrPlugin, and now basicSsl)Denning
Everything seems to be correct. Those VITE plugins don't create files in any of the folders that are watched. This is most likely just related to the Git issue I linked previously, which is the same issue that @LarsMunkholm brought up previously. Vite 4.3+ might improve your times, but otherwise you might be looking at either code splitting or adopting a different bundler (such as webpack).Catatonia
@JacobSmit Yeah, 4.3 isn't much help either unfortunately. I'll see how I can code split better and that should provide better results, but it's a real shame since we literally just migrated to ViteDenning
F
8

I was working on a quite big project and we had the exact same issue (over 3000 files requested on every "refresh" in development mode).

It has to do with 2 things:

1. the way you import files.

People generally make imports this way

import { Button } from "@package"

because it's easy and convenient.

Generally speaking, it's not a problem when you use bundles (webpack bundles), because everything is treeshaken loaded in a big shot.

When you use vite dev server, it's another story.

vite doesn't work like webpack. it's fast because it skips bundling, and basically files are loaded on the fly

Now what does it have to do previous hint on import syntax ?

When you do this

import { Button } from "@package"

What happens is the whole package exports are imported! even if you don't use them.

When you do this

import Button from "@package/Button"

only exports defined in @package/components/Button are imported.

2. Your configuration file

It might help if you can use some custom prebundling, but vite already does it quite good by design.

Long story short: Avoid index imports AND You have to change your imports to do them the right way.

The only good news here is that you can automate import transformations across your code easily with some metacoding using jscodeshift

We ended up with 120 requests instead of 3000 by doing it.

Faso answered 18/5, 2023 at 12:29 Comment(4)
how do you count the amount of files loaded?Ronnie
@NeuberOliveira sorry for late answer, but basicaly you look into your console tab and check http requests after filtering on .jsx or .tsx files. total number of requests is indicated right below list of filtered requests.Faso
@millenion, could you please specify how exactly you did that with jscodeshift? Did you write your transformers? Please share this codeAmused
@Amused The is already an import transformer example on the official website.Faso
Y
4

Based on the limited information, the issue seems to be is related to the initial loading time of your application rather than Vite.

The app behavior we know is, initial page load time is large, further reloads also take large time to load main page components that need re-rendering. Components that don't need re-rendering are not a problem (e.g., header/navbar).

One idea: Just load hero section/primary component on initial page load, and other components load lazily.

Here are some thoughts though:

  1. Explore Performance Benchmarks: The only way to know for sure what's the main issue is, is to profile it, benchmark it. Go ahead: link. It'll provide you answers on how to optimize them.

    E.g., Also, locally, Lighthouse (Chrome DevTools) has excellent features for your very specific issue, as well as resources on how to mitigate them. Initial page load, First Contentful Paint, Total Blocking Time, Critical rendering path, Image optimizations, Best Practices, SEO, Progressive web applications, etc. Here's an example benchmark of one of my site (For more details, it's just a blog site though):

    enter image description here

  2. Dynamically import modules/Code Splitting: Based on package.json, we can see there are a lot of third party dependencies, e.g., Auth, Firebase, Date, etc., which we don't need to load on initial page load. Good thing, Vite supports importing modules on demand (see example below).

    import('./myModule').then((module) => {
      // Use the module here
    });
    
  3. Dependency pre-bundling (by Vite): It does pre-bundling of dependencies of the package to optimize the dev/prod build. When running vite dev, it does that on the first load to optimize subsequent reloads. You might be issuing this in your dev experience, I'd say don't worry about it if it doesn't affect your prod app.

    Some packages ship their ES modules builds as many separate files importing one another. For example, lodash-es has over 600 internal modules! When we do import { debounce } from 'lodash-es', the browser fires off 600+ HTTP requests at the same time! Even though the server has no problem handling them, the large amount of requests create a network congestion on the browser side, causing the page to load noticeably slower.

    By pre-bundling lodash-es into a single module, we now only need one HTTP request instead!

    Note: Dependency pre-bundling only applies in development mode, and uses esbuild to convert dependencies to ESM. In production builds, @rollup/plugin-commonjs is used instead.

  4. Lazy Loading: It is a way to shorten the length of the critical rendering path, which we are aiming to do so. Load components lazily that are not immediately visible on the screen (e.g., all other components which do no fit in a browser window size, e.g., say just hero component + header/navbar, or just one row of charts/graphs).

    You can use React.lazy() to lazily load components. If your app is dynamic and highly interactive (+ huge 3rd party deps), e.g., a dashboard (which we see on the image, your app is), it is recommended to use it heavily on non-visible components. I'd say add react-suspense to all other components, so that the end-user won't see a straight blank white screen. See example of lazy():

     const MyComponent = React.lazy(() => import('./MyComponent'));
    

    JS ecosystem provides lazy loading strategies for most of the application assets:

    1. Code splitting.
    2. Script type module.
    3. CSS optimizations: Here we can see you are using Tailwind as well as Styled components/CSS in JS(x) – emotion.js. We can think about optimizing CSS files.
    4. Preloading web font resources using <link rel="preload"> and the CSS font-display property.
  5. Make 3rd party (e.g., CDN) requests asynchronous: This is a very debated as well as use-case dependent, but you can optimize your page load time, if well optimized.

  6. Push less Assets (JavaScript) to the client: It is the very obvious way of saying things, but there are ways we can think and explore this on a case-by-case basis. Tools like UglifyJS or Terser exists to minify and compress your code. Optimize static assets (Images, CSS, JS) by third-party tool and push them onto a private/public CDN. Cache heavily, as much as you can, use fingerprinting to let browsers identify a new JS/CS change.

    Now, that react out-of-the-box supports Server components (just like the heavy web frameworks), I'd say do heavy computational tasks onto the server and just push rendered

    HTML onto the client. Example: Building 3rd-party charts, which require you to get data from an API, serialize/deserialize it, then render/re-render it onto a component.

    Also, you can explore O(1) frameworks that advocate solving minute/critical problems, e.g., pushing less JS code to client (e.g., HTMX, Astro).

Yaws answered 7/5, 2023 at 16:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.