diff --git a/content/posts/2023/2023-10-20-core-web-vital-react/3D-preview.gif b/content/posts/2023/2023-10-20-core-web-vital-react/3D-preview.gif
new file mode 100644
index 000000000..f9f1e6415
Binary files /dev/null and b/content/posts/2023/2023-10-20-core-web-vital-react/3D-preview.gif differ
diff --git a/content/posts/2023/2023-10-20-core-web-vital-react/after.png b/content/posts/2023/2023-10-20-core-web-vital-react/after.png
new file mode 100644
index 000000000..c6267d8ea
Binary files /dev/null and b/content/posts/2023/2023-10-20-core-web-vital-react/after.png differ
diff --git a/content/posts/2023/2023-10-20-core-web-vital-react/before.png b/content/posts/2023/2023-10-20-core-web-vital-react/before.png
new file mode 100644
index 000000000..145414200
Binary files /dev/null and b/content/posts/2023/2023-10-20-core-web-vital-react/before.png differ
diff --git a/content/posts/2023/2023-10-20-core-web-vital-react/cover.jpg b/content/posts/2023/2023-10-20-core-web-vital-react/cover.jpg
new file mode 100644
index 000000000..eff7f5e3b
Binary files /dev/null and b/content/posts/2023/2023-10-20-core-web-vital-react/cover.jpg differ
diff --git a/content/posts/2023/2023-10-20-core-web-vital-react/index.mdx b/content/posts/2023/2023-10-20-core-web-vital-react/index.mdx
new file mode 100644
index 000000000..ce2403ee4
--- /dev/null
+++ b/content/posts/2023/2023-10-20-core-web-vital-react/index.mdx
@@ -0,0 +1,220 @@
+---
+layout: post
+title: Improving Core Web Vitals of a React Application
+description: A case study of changes implemented to improve the performances of a React application.
+slug: improving-core-web-vitals-react-app
+date: 2023-10-20
+language: en
+cover: ./cover.jpg
+tags:
+ - 'JavaScript'
+---
+
+import Info from '../../../../src/components/MDX/Info'
+import Warning from '../../../../src/components/MDX/Warning'
+
+The application I’m working on recently had a significant change: we decided to go from something
+wholly closed (behind a connection wall) to something more open. Which means, searchable on the
+internet.
+
+
+
+[Partfox](https://ui.orderfox.com/search) is a search engine that facilitates connexions between
+manufacturers and buyers in the CNC industry. In a nutshell, the user can post a technical drawing
+on the platform and we show them manufacturers that could make their workpiece.
+
+Technically it’s a single-page application built with React, TypeScript, and Vite. It’s hosted on
+Heroku.
+
+
+
+Two big topics came to the top of my priorities: SEO and Web Performance.
+
+As per usual, I started with a Google Lighthouse audit. The result was far from great.
+
+![before.png](./before.png)
+
+
+ 💡 **Tips:** when you do an audit with Lighthouse, always use your browser in incognito mode! In
+ normal mode, your AdBlockers automatically block 3rd party libraries, which makes the website load
+ faster.
+
+
+With the help of Google and Lighthouse, I ended up with a large to-do list.
+
+## Step 1: lazy loading images
+
+By default, images in the browser load immediately. It can lead to a bottleneck when you try to
+print many images on one page. To prevent this, you can change this default attribute and
+[lazy load](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#loading) images.
+
+```jsx
+// Before
+
+
+// After
+
+```
+
+In a nutshell, you can defer the image loading. When your screen is about to see the image, it loads
+it automatically.
+
+
+
+
+ Having too much lazy loading on your page can negatively impact your performance ([see
+ why](https://web.dev/lcp-lazy-loading)).
+
+
+## Step 2: using an image CDN
+
+Most of our assets (images, pdf, video) are hosted on a S3 bucket. When your app directly points to
+a S3 bucket you have zero compression, no image optimisation and it uses HTTP 1.1.
+
+Using an image CDN addresses these issues. We picked Cloudinary which is a paid product. We decided
+to go for this tool for a simple reason: it’s easy to use.
+
+```jsx
+// before
+https://s3-bucket.com/image.jpg
+
+// after
+https://res.cloudinary.com/{CLOUDINARY_ID}/image/fetch/w_80,h_80,c_thumb/https://s3-bucket.com/image.jpg
+```
+
+The 2nd URL will return a thumbnail of 80x80px of the original link. It’s automatically served with
+http/2 and the HTTP response has the right headers (for caching).
+
+## Step 3: code splitting (with react-router)
+
+{/* https://orderfox.slack.com/archives/C01E7P2ACCV/p1647428702147079?thread_ts=1647428373.269069&cid=C01E7P2ACCV */}
+
+For routing, we use React Router for linking routes with components. Our routing system was similar
+to this one:
+
+```jsx
+// Before
+import { createBrowserRouter } from 'react-router-dom'
+import SearchLanding from '@/modules/search/SearchLanding'
+import ResetPassword from '@/modules/account/ResetPassword'
+
+export const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ children: [
+ {
+ path: 'search',
+ element: ,
+ },
+ {
+ path: 'reset-password',
+ element: ,
+ },
+ // ...
+ ],
+ },
+])
+```
+
+This generates a big JS file with the entire application. It’s a pity to load the entire application
+when you only want to see one page.
+
+Fortunately, this problem can be addressed with React Suspense.
+
+```jsx
+// After
+import { lazy, Suspense } from 'react'
+import Loader from '@/modules/common/Loader'
+
+const SearchLanding = lazy(() => import('@/modules/search/SearchLanding'))
+const ResetPassword = lazy(() => import('@/modules/account/ResetPassword'))
+
+export const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ children: [
+ {
+ path: 'search',
+ element: (
+ }>
+
+
+ ),
+ },
+ {
+ path: 'reset-password',
+ element: (
+ }>
+
+
+ ),
+ },
+ // ...
+ ],
+ },
+])
+```
+
+With this, the application will be bundled into many files. So _“the user only loads what is
+needed”_. When the user goes from one page to another, he will see the loader while the component is
+being loaded.
+
+## Step 4: Using a CDN for HTTP/2
+
+In the previous step, I show you how we went from 1 big JS file to many files (~80). But, loading
+what you already need can be very costly. Mostly when you’re only using HTTP/1.1. This protocol only
+allows downloading one file at a time.
+
+One of the biggest additions of HTTP/2 (h2) is the
+“[Request and response multiplexing](https://web.dev/performance-http2/#request-and-response-multiplexing)”.
+When it came out, it was like a revolution in the browser. It allows the browser to download
+multiple files with a single request. Because “a picture is worth a thousand words”,
+[here’s a cool animation](https://freecontent.manning.com/animation-http-1-1-vs-http-2-vs-http-2-with-push/)
+if you want to better understand why h2 is faster.
+
+Our application was running on a Heroku dyno. Unfortunately, they do not support h2 (at the time
+being). To get it, while keeping our Heroku infrastructure, the solution is to use a CDN. CloudFront
+(AWS product) was the most relevant pick for us because we already had a few tools using the AWS
+suite. Same as our image CDN, CloudFront gives us a bunch of good additions such as h2, optimised
+caching… But it’s a hell of a mess to configure.
+
+## Step 5: lazy load big components
+
+Our coolest component is also the worst in terms of performance. When a user uploads a technical
+drawing, we render it in 3D. Under the hood, it uses a bunch of three.js libraries which render HTML
+canvas. The component itself weighs ~2Mb (thanks Three.js!).
+
+![3D-preview.gif](./3D-preview.gif)
+
+Unfortunately for us, these technical drawings can also be cumbersome. The file needed to display
+the canvas ranges from 1 to ~12MB. On our feed page, we can display a lot of them.
+
+I ended up with a solution mixing React.suspense and a custom hook:
+
+- React.suspense - to defer loading the JS assets needed to show the component.
+- A [`useIsVisibleOnScreen` hook](https://gist.github.com/maxpou/f235ef5b2e0f63b3a32e78f484b081c8) -
+ to defer loading the assets needed to show the 3D piece. In a nutshell, this hook detects if the
+ component is in the viewport or if he is about to be seen.
+
+## Outcome
+
+…after I these changes, I did another bunch of audits and tada! I finally got a more decent score 🎉
+
+![after.png](./after.png)
+
+I know what you think: it could be 100.
+
+But, this is not a side project, nor a “Hello World” project. Focusing on the last 10 remaining
+points would be too time-consuming for me. It’s not worth the trouble.
+
+What I liked about this journey was the simplicity of the actions to obtain a decent performance
+score. I mean, it’s not rocket science. I mostly followed tips given by Lighthouse and other best
+practices that can easily be found on the internet.