LECTURE: OPTIMIZING WEB APPLICATIONS WITH CODE SPLITTING AND TREE SHAKING
MOTIVATION
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.
SECTION 1: UNDERSTANDING CODE SPLITTING
WHAT IS CODE SPLITTING?
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:
* Client-side routes: Homepage, post list, filtered post views (e.g., by tag or
date), and individual post pages.
* Admin routes: Dashboard, post editor, and post management.
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.
HOW DOES IT WORK?
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:
* The homepage shows a list of recent posts.
* The admin dashboard is only accessible to logged-in admins.
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.
EXAMPLE: IMPLEMENTING CODE SPLITTING
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:
1. Dynamic Import: The import('./components/AdminDashboard') statement tells
the bundler to create a separate chunk for AdminDashboard.
2. Lazy Loading: React.lazy() wraps the dynamic import, ensuring the component
is only loaded when the /admin route is accessed.
3. Suspense: The Suspense component shows a fallback UI (e.g., “Loading...”)
while the chunk is being fetched.
4. Result: Users visiting the homepage don’t download the admin dashboard code,
reducing the initial bundle size.
ADVANTAGES AND DISADVANTAGES
Advantages:
* Faster initial load times, especially for large apps.
* Better user experience, as pages load only what’s needed.
* Efficient use of network resources.
Disadvantages:
* Adds complexity to the codebase (e.g., handling loading states).
* May introduce slight delays when loading new chunks, requiring fallback UIs.
* Requires compatible tools (e.g., Webpack, Vite) and proper configuration.
ADVANCED EXAMPLE
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.
SECTION 2: UNDERSTANDING TREE SHAKING
WHAT IS TREE SHAKING?
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.
COMMON MISCONCEPTION
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).
HOW DOES IT WORK?
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:
* Use ES modules (not CommonJS).
* Enable production mode in your bundler (e.g., mode: 'production' in Webpack).
* Avoid side effects in modules that could prevent tree shaking.
EXAMPLE: IMPLEMENTING TREE SHAKING
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:
1. ES Modules: The utils.js file uses export, making it tree-shakable.
2. Selective Import: We only import formatDate, not the entire utils module.
3. Bundler Analysis: In production mode, Webpack or Vite analyzes the code and
sees that sanitizeInput and generateSlug are unused.
4. Result: The final bundle excludes 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 AND DISADVANTAGES
Advantages:
* Smaller bundle sizes, leading to faster load times.
* Works automatically with modern bundlers in production mode.
* Encourages modular code design.
Disadvantages:
* Limited to ES modules; CommonJS modules aren’t tree-shakable.
* Side effects in modules (e.g., global state changes) can prevent tree
shaking.
* Requires careful module design to maximize benefits.
ADVANCED EXAMPLE
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.
SECTION 3: COMBINING CODE SPLITTING AND TREE SHAKING
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:
* Code Splitting: Separates client routes (e.g., homepage, post list) from
admin routes (e.g., dashboard).
* Tree Shaking: Removes unused utilities or library functions from each chunk.
EXAMPLE: OPTIMIZED 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:
* Code Splitting: The AdminDashboard and TagFilteredPosts components are
lazy-loaded, creating separate chunks.
* Tree Shaking: The sanitizeInput function in utils.js is excluded because it’s
unused.
* Vite Config: The 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.
CONCLUSION
KEY TAKEAWAYS
* Code Splitting: Breaks your app into smaller chunks, loading only what’s
needed for a page or feature. Use React.lazy() and dynamic imports to
implement it.
* Tree Shaking: Removes unused code from your bundles, making them leaner. Use
ES modules and production mode to enable it.
* Combined Power: Together, these techniques drastically reduce bundle sizes
and improve load times, as seen in our blogging app.
* Best Practices:
* Use modern bundlers like Webpack or Vite.
* Write modular, side-effect-free code for maximum tree shaking.
* Test your bundles with tools like Webpack Bundle Analyzer to verify
optimizations.
COMMON PITFALLS TO AVOID
* Don’t rely on modular code alone; configure your bundler for tree shaking.
* Avoid CommonJS modules, as they aren’t tree-shakable.
* Ensure fallback UIs for code splitting to handle loading states gracefully.
* Test in production mode, as development mode often disables optimizations.
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!
Maggie is a generative AI that can help you understand the course content better. You can ask her questions about the lecture, and she will try to answer them. You can also see the questions asked by other students and her responses.
Join the discussion to ask questions, share your thoughts, and discuss with other learners