Next.js Optimization For Better Performance - Part 1 : Material UI (MUI), Configs & Plugins

Yashas H R
6 min readJan 6, 2023

--

I have been keeping an eye on Next.js for a very long time now, testing out every major version changes, and recently built an app for a client (also a friend) and wanted to give out some info on optimizing Next.js.

Starting a new project

When starting a new project, remember to always check for a template that might fit your needs in here
https://github.com/vercel/next.js/tree/canary/examples

For Material UI it was here:
https://github.com/vercel/next.js/tree/canary/examples/with-material-ui

Since I use JavaScript, I started my project by cloning
https://github.com/mui/material-ui/tree/master/examples/material-next

If at all you already have a project created and in production then you’ll have to adopt the changes given in the example’s for best performance. I had originally missed this and instead got my code from an old blog post with similar content to the with-material-ui example.

But remember that there’s a big difference in copying code from a github repository and a blog post, and that is it’s updates.

In github repository, the code is updated as required along with each of change to next.js. But most of the time blog posts are NOT updated meticulously and continuously and hence you might end up with an old and unoptimized code if you are copying it from blog posts. So in my opinion, copying codes from blog posts is not a good choice.

Current Next.js Version

Currently when I’m writing this post, Next.js version is 13.1.1.

Next.js 13.1 released just 2 weeks back and some amazing new features added yet again. The blog post can be seen here:
https://nextjs.org/blog/next-13-1

Next.js Transpile Modules

What caught my eye in the recent release was Built-in module transpilation (stable). You might or might not know but next-transpile-modules was a famous package that was used with Next.js for transpilation which was instantly depricated when Next.js 13.1 released. When I found that 13.1.1 was released, I moved on to it and also updated Material UI(MUI) 5.11.3 and the project broke. Turns out I had to now add MUI packages transpilePackages config in next.config.js thus making it like below.

transpilePackages: ["@mui/system", "@mui/material", "@mui/icons-material"],

Modularize Imports

Now this is actually more important than you think. When I’m using MUI, I always import modules using top level or named imports, i.e. something like below

import { Add as AddIcon } from '@mui/icons-material';
import { Tabs } from '@mui/material';

Before I used modularize imports I was loading about thousands of modules just for one page, among which only 10% was really used, and all others were unused imports because it just comes along with the way I’m doing the import.

You might ask me why not use 2nd level import like this

import AddIcon from '@mui/icons-material/Add';
import Tabs from '@mui/material/Tabs';

Which would load only the modules that I need and not load unnecessary codes, but this is not developer friendly. This is definitely end client friendly and build friendly but, I would then have to write separate imports for each component I want to import from MUI, whereas in 1st level imports I could load multiple modules like this

import {
Box,
Divider,
FormControl,
Grid,
InputLabel,
MenuItem,
Select,
Skeleton,
Typography,
} from "@mui/material";

Now imagine writing separate import line for each component. Also this is not Auto Import friendly as I have seen it if at all you use that feature/extension.

Now going back to the topic, using top level imports would cause many unnecessary codes and modules to be imported automatically. This was how many modules Next.js had to load for just one page as printed in the output:

event — compiled client and server successfully in 34.8s (23238 modules)

These are the imports being used in the same file

import {
Box,
Button,
Card,
CardContent,
CardMedia,
Chip,
Container,
Divider,
FormControl,
Grid,
InputLabel,
MenuItem,
Paper,
Rating,
Select,
Skeleton,
Typography,
} from "@mui/material";

Now here is where Modularize Imports comes into play.

Previously this feature was experimental, so this had to be added under experimental section of next.config.js, now it’s a stable feature from Next.js 13.1

All I had to add was a modularizeImports in next.config.js

modularizeImports: {
"@mui/material/?(((\\w*)?/?)*)": {
transform: "@mui/material/{{ matches.[1] }}/{{member}}",
},
"@mui/icons-material/?(((\\w*)?/?)*)": {
transform: "@mui/icons-material/{{ matches.[1] }}/{{member}}",
},
},

P.S. the above regex was something I found on internet when I was looking to optimize my code, you can try out your own simpler version if it’s working fine without any errors.

Now that I have added this, going back to the code now I see Next.js log printing for the same code as previous one

event — compiled client and server successfully in 4.9s (1937 modules)

Neat, tidy and done. Now you might still think that 1937 is large but it’s way better now than what it was before and I’m satisfied with it for now. Also it’s hard to keep track of what all modules the imported modules are dependent on. We can keep optimizing as we go on.

Advanced Security Headers

This whole section is based on what’s in here:
https://nextjs.org/docs/advanced-features/security-headers

Since most of the things I wanted to say is in that link I shared above, I’ll just show the code I’m using.

First I’ll declare a variable with all the headers I’m planning on using in next.config.js:

const advancedHeaders = [
{
key: "X-DNS-Prefetch-Control",
value: "on",
},
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
{
key: "X-XSS-Protection",
value: "1; mode=block",
},
{
key: "X-Frame-Options",
value: "SAMEORIGIN",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Referrer-Policy",
value: "origin-when-cross-origin",
},
];

And then add this to the configuration:

  async headers() {
return [
{
// Apply these headers to all routes in your application.
source: "/:path*",
headers: advancedHeaders,
},
];
},

Now we have our security headers setup and what it does is:

  • Prefetch DNS for faster connection.
  • Optimize HTTPS.
  • Protect against cross-site scripting (XSS) attacks.
  • Prevent clickjacking attacks.
  • Prevent XSS exploits for websites that allow users to upload and share files

Now as you can see, I’m not using Content-Security-Policy, which is usually recommended but this header is not compatible with most of the necessary services, especially if you are using external 3rd party services.

I use Google AdSense and one of their staff says:

When you consider the nature of AdSense — Google is serving ads that may come from a variety of networks and sources — it seems it is inconsistent to run ads with this sort of diverse source with a high level CSP. In other words if CSP is a vital aspect or your site, then perhaps AdSense (or for that matter any sort of third-party ad broker system) may not be an ideal match.

You can check the help thread here:
https://support.google.com/adsense/thread/102839782/content-security-policy-csp-settings-for-adsense?hl=en

Git Ignore

I use the below .gitignore for my repository.

# See https://help.github.com/ignore-files/ for more about ignoring files.

# dependencies
/node_modules

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Next.js
/.next

Plugins

I use more than one plugin with my Next.js project, and so I was using next-compose-plugins long time back. But it’s been 2 years now and still no update on the repository of the plugin. So the plugin is no longer compatible with the recent versions of Next.js and your project will break if you use it.

But not to worry, there’s a very simple natural fix for this.

Below is next-compose-plugins replacement code:

const nextConfig = {
transpilePackages: ["@mui/system", "@mui/material", "@mui/icons-material"],
modularizeImports: {
"@mui/material/?(((\\w*)?/?)*)": {
transform: "@mui/material/{{ matches.[1] }}/{{member}}",
},
"@mui/icons-material/?(((\\w*)?/?)*)": {
transform: "@mui/icons-material/{{ matches.[1] }}/{{member}}",
},
},
async headers() {
return [
{
// Apply these headers to all routes in your application.
source: "/:path*",
headers: advancedHeaders,
},
];
},
};

module.exports = async (phase) => {
/**
* @type {import('next').NextConfig}
*/

const plugins = []; //All your plugins go into this array

return plugins.reduce((acc, next) => next(acc), nextConfig);
};

Using the above code you are no longer dependent on any external package to run multiple plugins with your Next.js project.

Now starting off with some of the basic necessary plugins and packages that you might need, here is a list:

Remember that not all the above listed packages can be added to plugins array. Only next-pwa and @next/bundle-analyzer needs to be added to plugins array, all others listed are to be used directly.

Each package listed above is linked to their documentations and it’s best to read it there to be updated with latest changes.

Thank you for reading, in the next post Part 2, I’ll be posting about Lighthouse, LCP, CLS, Speed Index and many more optimization.

--

--

Yashas H R

Python 3 Dev | UX/UI/Web Designer | Full Stack Dev | Video Encoding Researcher(FFMPEG) | Automation in Python