Welcome to today’s lecture on optimization techniques in full stack development, specifically code splitting and tree shaking. These are powerful tools to make your web applications faster, leaner, and more efficient—crucial skills for any modern developer. Why does this matter? Imagine you’re building a blogging application where users can browse posts, filter them by tags or dates, and admins can manage content. If the app loads slowly because it’s trying to send a massive JavaScript bundle to the user’s browser, they’ll get frustrated and leave. Slow apps lose users, and nobody wants that!
Let’s set the scene with a relatable problem: You’ve built a blogging app, but the initial load takes 10 seconds because the entire JavaScript codebase—client-side views, admin dashboard, and all utilities—is sent to the user’s browser in one giant file. Code splitting and tree shaking can solve this by reducing what’s sent to the browser and removing unused code. For example, why load the admin dashboard code for a regular user who just wants to read a blog post?
A common misconception is that simply writing modular code means your app is optimized. Not true! Without deliberate optimization, your bundles might still include unnecessary code. Another myth is that these techniques are only for huge apps like Netflix or Amazon. Even our blogging app can benefit significantly from smaller, faster-loading bundles.
Let’s dive in and explore how code splitting and tree shaking can transform our blogging app, making it snappy and user-friendly.
Code splitting is a technique where you break your application’s JavaScript bundle into smaller chunks, loading only what’s needed for a specific page or feature. This reduces the initial load time and improves performance, especially for users on slower networks or devices.
In our blogging app, we have:
Without code splitting, the browser downloads everything upfront, even if a user only visits the homepage. With code splitting, we can load the homepage code first and fetch other parts (like the admin dashboard) only when needed.
Code splitting leverages dynamic imports in JavaScript, allowing you to load modules asynchronously. Modern frameworks like React and bundlers like Webpack or Vite make this easy. For example, in React, you can use React.lazy()
to load components only when they’re rendered.
Let’s look at a simple example in our blogging app:
Without code splitting, both the homepage and admin dashboard code are bundled together. With code splitting, we can load the admin dashboard code only when an admin navigates to it.
Here’s how we can split the admin dashboard in a React-based blogging app using React.lazy()
and Suspense
:
// App.jsx
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import HomePage from './components/HomePage';
// Lazy-load the AdminDashboard
const AdminDashboard = lazy(() => import('./components/AdminDashboard'));
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/admin" element={<AdminDashboard />} />
</Routes>
</Suspense>
</Router>
);
}
export default App;
Step-by-Step Explanation:
import('./components/AdminDashboard')
statement tells the bundler to create a separate chunk for AdminDashboard
.React.lazy()
wraps the dynamic import, ensuring the component is only loaded when the /admin
route is accessed.Suspense
component shows a fallback UI (e.g., “Loading...”) while the chunk is being fetched.Advantages:
Disadvantages:
Now, let’s extend code splitting to the post filtering feature. Users can filter posts by tags or dates. Each filter view could be a separate component, lazily loaded to avoid bundling all filter logic upfront.
// PostList.jsx
import React, { Suspense, lazy } from 'react';
import { useParams } from 'react-router-dom';
const TagFilteredPosts = lazy(() => import('./TagFilteredPosts'));
const DateFilteredPosts = lazy(() => import('./DateFilteredPosts'));
function PostList() {
const { filterType, value } = useParams(); // e.g., /posts/tag/javascript or /posts/date/2023
return (
<Suspense fallback={<div>Loading posts...</div>}>
{filterType === 'tag' && <TagFilteredPosts tag={value} />}
{filterType === 'date' && <DateFilteredPosts date={value} />}
</Suspense>
);
}
export default PostList;
Here, the TagFilteredPosts
and DateFilteredPosts
components are loaded only when their respective routes are accessed, further optimizing the app.
Tree shaking is a technique to eliminate dead code—code that’s included in your bundle but never used. It’s like pruning a tree, removing branches (code) that don’t contribute to the app’s functionality. This is especially useful for removing unused exports from libraries or utility modules.
In our blogging app, suppose we use a utility library with functions like formatDate
, sanitizeInput
, and generateSlug
. If we only use formatDate
, tree shaking ensures the other functions aren’t included in the final bundle.
Students often think that importing only what they need (e.g., import { formatDate } from 'utils'
) automatically optimizes the bundle. Not quite! Without tree shaking, the entire utils
module might still be included. Tree shaking requires specific conditions, like using ES modules and a bundler that supports it (e.g., Webpack, Rollup, or Vite).
Tree shaking relies on the static structure of ES modules (import
/export
). Bundlers analyze the code to determine which exports are used and exclude the rest. For tree shaking to work:
mode: 'production'
in Webpack).Let’s create a utility module for our blogging app and demonstrate tree shaking.
// utils.js
export function formatDate(date) {
return new Date(date).toLocaleDateString();
}
export function sanitizeInput(input) {
return input.replace(/<[^>]*>/g, '');
}
export function generateSlug(title) {
return title.toLowerCase().replace(/\s+/g, '-');
}
Now, in our PostList
component, we only use formatDate
:
// PostList.jsx
import { formatDate } from './utils';
function PostList({ posts }) {
return (
<div>
{posts.map(post => (
<div key={post.id}>
<h2>{post.title}</h2>
<p>Posted on {formatDate(post.date)}</p>
</div>
))}
</div>
);
}
export default PostList;
Step-by-Step Explanation:
utils.js
file uses export
, making it tree-shakable.formatDate
, not the entire utils
module.sanitizeInput
and generateSlug
are unused.sanitizeInput
and generateSlug
, reducing its size.To test this, students can build the app in production mode and inspect the bundle (e.g., using Webpack’s bundle analyzer). They’ll see that only formatDate
is included.
Advantages:
Disadvantages:
In the admin dashboard, we might use a library like Lodash. Instead of importing the entire library, we can import specific functions to enable tree shaking:
// AdminDashboard.jsx
import { debounce } from 'lodash-es'; // Use lodash-es for ES modules
function AdminDashboard() {
const handleSearch = debounce((query) => {
console.log(`Searching for ${query}`);
}, 300);
return (
<input type="text" onChange={(e) => handleSearch(e.target.value)} />
);
}
export default AdminDashboard;
By using lodash-es
and importing only debounce
, we ensure that unused Lodash functions (e.g., map
, filter
) are excluded from the bundle.
Code splitting and tree shaking complement each other. Code splitting divides the bundle into smaller chunks, while tree shaking ensures each chunk contains only the necessary code. In our blogging app:
Let’s combine both techniques in a more complete example. Below is a simplified structure of our blogging app, assuming we’re using React, React Router, and Vite.
// App.jsx
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import HomePage from './components/HomePage';
import PostList from './components/PostList';
const AdminDashboard = lazy(() => import('./components/AdminDashboard'));
const TagFilteredPosts = lazy(() => import('./components/TagFilteredPosts'));
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/posts" element={<PostList />} />
<Route path="/posts/tag/:tag" element={<TagFilteredPosts />} />
<Route path="/admin" element={<AdminDashboard />} />
</Routes>
</Suspense>
</Router>
);
}
export default App;
// components/PostList.jsx
import { formatDate } from '../utils';
function PostList({ posts = [] }) {
return (
<div>
{posts.map(post => (
<div key={post.id}>
<h2>{post.title}</h2>
<p>Posted on {formatDate(post.date)}</p>
</div>
))}
</div>
);
}
export default PostList;
// utils.js
export function formatDate(date) {
return new Date(date).toLocaleDateString();
}
export function sanitizeInput(input) {
return input.replace(/<[^>]*>/g, '');
}
Vite Configuration (to enable tree shaking and code splitting):
// vite.config.js
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
admin: ['./src/components/AdminDashboard'],
tagFilter: ['./src/components/TagFilteredPosts'],
},
},
},
},
};
What’s Happening:
AdminDashboard
and TagFilteredPosts
components are lazy-loaded, creating separate chunks.sanitizeInput
function in utils.js
is excluded because it’s unused.manualChunks
option ensures admin and tag filter code are in separate chunks, enhancing code splitting.Students can test this by running npm run build
with Vite and inspecting the output in the dist
folder. They’ll see separate chunks for admin
and tagFilter
, and the bundle analyzer will confirm that sanitizeInput
is excluded.
React.lazy()
and dynamic imports to implement it.By applying code splitting and tree shaking to our blogging app, we’ve made it faster and more efficient, ensuring users get a smooth experience whether they’re reading posts or managing content. Keep experimenting with these techniques in your projects, and you’ll see significant performance gains!
Boost your blogging app’s speed with code splitting and tree shaking! Split JS into chunks for faster loads and trim unused code to shrink bundles. Learn dynamic imports, React lazy, and ES modules with real examples. Debunk myths, optimise like a pro, and make your app lightning-fast!
Let's talk about further optimisations for your apps allowing you to limit the bundle that you serve to your clients.
We know that slow apps lose users. Nobody wants a blogging app that takes ages to load! Optimisation ensures that users stay engaged.
There are two powerful mechanisms to speed up the loading of your app. First, there is code splitting, which allows the browser to load only what’s needed by dividing JS bundles and sending just the code for the current page, such as the homepage or admin dashboard.
Second, tree shaking removes unused code from bundles, making them leaner and faster to download. Let's explore them both!
Let's explore code splitting first.
Code splitting divides your app’s bundle so users download only what they need for a page.
To split the bundle, you have to use javaScript’s dynamic imports to load modules on-demand, supported by tools like Webpack (which is part of NextJS) or Vite.
A good use case is to load code related to a React component, only when that component is rendered.
Let's see an example! This code splits our blogging app’s bundle. The homepage loads instantly, while the admin dashboard is fetched only when needed, reducing initial load time.
This line uses the lazy react component in combination with a dynamic import. The bundler will create a separate JS bundle for all code that relates to the AdminDashboard component.
Last, we configure our application only to render this component when an admin route is detected. Pure profit since we do not need to send our standard blog users any javascript related to admin functionality! Also, please note that this is crucial in client-rendered components and not necessary for server-rendered components as those send only component-related javascript to the user.
Tree shaking is yet another optimisation. The bundler takes care of everything, and you only have to adhere to specific rules to make it work. Let's discuss them!
Tree shaking eliminates dead code, like unused functions or variables, from the final bundle.
Remember, it unly works with ES modules using import/export, not CommonJS. The reason is that tree shaking requires static analysis of all imports to estimate which code is not used.
A good example of tree shaking is when you use large utility libraries like lodash; only the functions you use will make it to the final bundle during build time.
Let's check out an example, where tree shaking ensures only formatDate is included in the bundle, as sanitizeInput is unused, shrinking the app’s size.
We have our utility library with two functions, formatDate and sanitizeInput.
But, in our code we only import the formatDate function.
When we build our app, we can see how code splitting and tree shaking reduce bundle size. Before using these optimisations, the entire app loads at once. After we load only the homepage code and trim unused utilities, we make the app faster.
Let's wrap up!
Keep your client-side bundle lean and load only what is needed. This improves both application load times and speed. To do this, use dynamic imports, leveraging React lazy and import function for code splitting.
Tree shaking only works in production builds with bundlers like Vite or Webpack. Make sure you carefully check for changes in your bundle sizes.
To fully take advantage of tree shaking, avoid side effects in your functions, and try to use only libraries that support ES modules. Avoid common JS in your code at all costs. Follow these practices to optimise effectively. All the best!