โ ESSAY
The following is feedback sent by an actual developer.
It was so helpful that I kept saying "wow..." the entire time I was reading it. Thank you so much for the detailed analysis.
While building my portfolio with an animation library, this was the first time I really thought about web performance. I kept poking around the Performance and Lighthouse tabs and looked up various documents on things like reducing JavaScript bundles, but the material was unfamiliar and hard to work through on my own.
Then I requested the analysis service from you, and I was genuinely impressed by how detailed and professional the analysis was. You introduced not only various strategies for web performance optimization but also principles that account for SEO and accessibility, and even showed real applied examples, which helped me tremendously.
There were times during development when I felt uneasy and suspicious about something but just let it slide. The part where the intro animation plays every time before the main content appears was exactly that. It made me realize that emphasizing animation could end up lowering both performance and accessibility, and I now feel I should develop in line with the Progressive Enhancement principle you introduced.
I'll study the analysis in detail and apply the improvements to my portfolio one by one. This is truly the best.. thank you sincerely.
Absolutely, without hesitation. I've never seen material that covers performance in this much detail. When it's published, I'll definitely buy it and read it!
When implementing animations, I'm curious whether implementing them with CSS transitions is better for performance than using a library. I think the approach you demonstrated of adding classes via the classList.add() method is effective, but I know libraries like GSAP also do internal optimizations, and since I don't really know how they compare performance-wise, I kept having doubts.
For simple transition effects, CSS transitions with no dependencies whatsoever will obviously be more advantageous. (There's zero JavaScript overhead, and you're using only CSS.)
However, for complex cases or situations that require user interaction, GSAP should likewise be good enough that you won't feel any performance penalty.
I haven't used GSAP that much myself (I've mostly used framer-motion), but it's most likely built internally to take the GPU path well.
While researching animation performance, I heard the term GPU acceleration a lot, and I was curious how the GPU processes CSS. Also, even if the number of animated elements grows, can the browser still maintain 60fps and run smoothly as long as these principles are followed?
The browser automatically separates elements into their own layers and performs only compositing work on the GPU for elements that satisfy the following conditions.
https://www.chromium.org/developers/design-documents/gpu-accelerated-compositing-in-chrome/
And so on.. these properties skip the layout calculation and paint stages, and only the Composite stage runs on the GPU, which makes them very fast and smooth.
So even with many animations applied, you should be able to hit 60fps as long as the following conditions are met.
The following is the write-up delivered to the actual developer. They said everything could be made public, so it's published in full without any redaction.
Disclaimer
This summary condenses the key points from the performance analysis report of the portfolio-amber-mu-57.vercel.app website (as of July 19, 2025, 12:00). Depending on website updates or environmental changes after the time of analysis, the actual state may differ. This analysis was inferred from the bundled artifacts deployed to the browser, so it may differ somewhat from the code as actually written.
The performance bottlenecks and improvement suggestions presented are general recommendations, and their actual effect when applied may vary depending on many factors such as the website's specific implementation, server environment, and traffic patterns. This summary is provided for informational purposes, and the final decisions on applying the suggestions and responsibility for their outcomes rest with the website's owner.
For more detailed analysis, methodology, and full context, please refer to the original analysis report.
Hello! To help the https://portfolio-amber-mu-57.vercel.app/ website deliver a faster and more reliable user experience, I've summarized the key findings from a performance analysis of the currently deployed service.
The portfolio-amber-mu-57.vercel.app website is a single-page portfolio built around a GSAP-based intro animation and a CSR-centric React tech stack. It currently appears to be in an early stage of development, and in terms of animation presentation it delivers an impressive user experience. Structurally, some design choices work against it in performance measurement tools like Lighthouse, so there is clear room for improvement.
The main findings are as follows.
index.js bundlereact, react-dom, emotion, and gsap are included in one bundleAccordingly, I suggest prioritizing the following improvements.
opacity and transform.react and react-dom into separate chunks with the manualChunks option.localStorage or similarApplying these improvements should yield gains across many fronts: user experience at initial render, key performance metrics like LCP/FCP, search engine optimization, and mobile responsiveness. Please refer to the full report body for detailed technical context and rationale.
The website was analyzed as deployed on July 19, 2025.


The tools used for the analysis were:
Since this website has only a single JavaScript resource, I analyzed that resource to identify which frameworks and libraries are in use.
data-emotion and __EMOTION_TYPE_PLEASE_DO_NOT_USE__.useAtom and useSetAtom. Either jotai is in use, or a similar library was implemented or adapted directly.As the address makes clear, https://portfolio-amber-mu-57.vercel.app/ is currently a website deployed and served through the Vercel platform.
To get to the root cause of the developer's concern โ the low Lighthouse performance score โ I analyzed concretely which areas should be improved first.
One of the most bewildering moments in web performance analysis is when you measure the same page but the Performance tab and Lighthouse report completely different LCP values. In fact, while the developer was worried about a low Lighthouse score, WebPageTest and the Lighthouse tab inside DevTools were showing quite good scores.


The screenshots above show scores measured by webpagetest and Chrome DevTools respectively, and you can see the Lighthouse scores are all excellent.
But the Performance tab tells a different story.

Unlike the excellent Lighthouse scores we saw earlier, the Performance tab shows LCP dropping into the 4-second range. To understand why, we need to look at the differences in how the Performance tab and Lighthouse take their measurements.
The reason the tools differ isn't simple measurement error โ it comes down to differences in the logic and philosophy that decide when measurement ends.
The Performance tab has DevTools trace the browser's actual execution path as-is. Even after the load event finishes, it automatically records an extra 5 seconds in order to observe additional UI changes (e.g., JavaScript animations, rendering). You can find the relevant source in the Chromium codebase.
UI.panels.timeline._millisecondsToRecordAfterLoadEvent = 5000
The intent is to reflect the performance users actually perceive. Looking at the code, the variable name millisecondsToRecordAfterLoadEvent shows it waits about 5000ms. You can also roughly infer this by actually recording performance in the Performance tab.

It's not exact, but you can see it waiting roughly 5 seconds.
So what if we modified this number of seconds, re-measured, and checked whether the assumption holds โ that Lighthouse comes out lower? Here's how to modify that wait time. It may be a slightly excessive verification, but it sounds fun, so let's take a look. ๐

In this window, we can inspect the DevTools instance the website is using with another DevTools and perform whatever manipulation we want.UI.panels.timeline.millisecondsToRecordAfterLoadEvent = 3000 . Now the Performance panel, which used to wait 5 seconds, waits only 3.Measuring performance again, you can confirm that LCP comes out with a very similar, good score โ much like Lighthouse.

Then on what basis does Lighthouse decide when to score? To understand Lighthouse's measurement conditions, we need to look at Lighthouse's core logic.
To see when measurement ends, we need to look at the _waitForFullyLoaded method. This function measures the point at which the page finishes loading, and the following three conditions must be satisfied.
_waitForFullyLoaded(pauseAfterLoadMs, networkQuietThresholdMs, cpuQuietThresholdMs,
maxWaitForLoadedMs) {
let maxTimeoutHandle; // stores the timeout handle
// 1. Wait for the Load event (+ extra wait time)
// pauseAfterLoadMs: extra time to wait after the Load event (default: 0ms)
const waitForLoadEvent = this._waitForLoadEvent(pauseAfterLoadMs);
// 2. Wait for Network Quiet
// networkQuietThresholdMs: how long the network must stay quiet (default: 5000ms)
// What counts as quiet? 2 or fewer concurrent in-flight requests
const waitForNetworkIdle = this._waitForNetworkIdle(networkQuietThresholdMs);
// 3. Wait for CPU Quiet (initialized later)
let waitForCPUIdle = null;
// 4. Promise that runs once both Load and Network complete
const loadPromise = Promise.all([
waitForLoadEvent.promise, // wait for the Load event
waitForNetworkIdle.promise, // wait for Network Quiet
]).then(() => {
// Start checking the CPU only after the network has gone quiet!
// cpuQuietThresholdMs: how long the CPU must stay quiet (default: 0 = no check)
waitForCPUIdle = this._waitForCPUIdle(cpuQuietThresholdMs);
return waitForCPUIdle.promise;
}).then(() => {
// Return a cleanup function once all conditions are satisfied
return function() {
log.verbose('Driver', 'loadEventFired and network considered idle');
clearTimeout(maxTimeoutHandle); // cancel the timeout
};
});
// 5. Maximum wait timeout (safety net)
// maxWaitForLoadedMs: maximum wait time (default: 30 seconds)
const maxTimeoutPromise = new Promise((resolve, reject) => {
maxTimeoutHandle = setTimeout(resolve, maxWaitForLoadedMs);
}).then(_ => {
// Return a cleanup function on timeout
return function() {
log.warn('Driver', 'Timed out waiting for page load. Moving on...');
waitForLoadEvent.cancel(); // cancel all waits
waitForNetworkIdle.cancel();
waitForCPUIdle && waitForCPUIdle.cancel();
};
});
// 6. Race: normal completion vs. timeout, whichever finishes first
return Promise.race([
loadPromise, // normal measurement completion
maxTimeoutPromise, // 30-second timeout
]).then(cleanup => cleanup()); // run the cleanup function
}
This function checks the three conditions below in order, and if any of them isn't satisfied, it force-terminates after maxWaitForLoadedMs (default: 30 seconds).
pauseAfterLoadMs)
0ms, so it moves on to the next condition immediately after the Load eventnetworkQuietThresholdMs)
5000ms"network-2-quiet" statecpuQuietThresholdMs)
cpuQuietThresholdMs (e.g., 5000ms) must pass after the last Long Task endscpuQuietThresholdMs is 0, this check is skippedSummarizing everything so far in a table:
| Item | Performance tab | Lighthouse |
|---|---|---|
| Purpose | Real-time behavior recording | Quality measurement in a controlled environment under specific conditions |
| End criterion | load event + auto-stop 5 seconds later | Ends when Load, Network, and CPU Idle conditions are met (or 30s timeout) |
| LCP collection window | Can detect content up to its last appearance | Elements appearing after early termination are not reflected |
| Measured value | Close to the actual user experience | Based on experimental criteria (variables can be controlled) |
The reason LCP measures slower in the DevTools Performance tab is that its purpose is to detect the moment the user actually sees the content. Lighthouse, on the other hand, ends measurement early once it judges the page has stabilized enough, and any large content that appears afterward is not included as an LCP candidate.
So Lighthouse reporting a low LCP while the Performance tab reports a high one is not mere error โ it should be understood as a difference in the tools' design goals. Once you understand this difference, you can more clearly judge which tool's result reflects the real problem.
Of course, the most accurate performance measurement is RUM (Real User Monitoring) based on real user data. Lighthouse and DevTools are merely auxiliary means of approximating it โ don't forget that the real answer lies not in the browser but in the user's eyes.
Earlier I explained that the LCP measurement gap between Lighthouse and the Performance tab arises from differences in the criteria that determine when measurement ends. In this section, based on the website's rendering flow, let's analyze concretely why LCP is measured relatively later in the Performance tab.
At initial page entry, the div#root element exists empty, with no content. Since there are no meaningful elements displayed within the viewport at initial render, there is also no LCP candidate. The browser interprets this as a state in which no visual content can be initially detected.
Afterwards, elements with class names like .introTitle and .introTitleFill appear, and a short self-introduction phrase shows up on screen. This behavior plays as sequences through a GSAP-based custom animation implementation. At this point, that text block would have been recorded as the LCP candidate. Indeed, looking at webpagetest, you can see that this area was recorded as the LCP.

After the self-introduction phrase appears, a fade-out animation is applied to the entire .introTitleSection. That area is visually removed via opacity and Y-axis translation, and then the website's actual main content renders. The key code used in this stage is as follows.
// Judging from their blog posts, I'd guess they built a library? function? called Rally.
Rally({
target: '.introTitleSection',
motions: [
{
delay: 0.4,
duration: 0.6,
ease: 'back.in',
opacity: {
to: 0,
},
translateY: {
to: -30,
},
},
],
})
After this animation sequence ends, a callback that renders the actual content runs, and from that point the website's core content mounts and appears within the viewport.
Here it's worth recalling what we discussed in part 4-0.
updateStep callback described above runs, the self-introduction text โ not the actual content โ is recorded as the final LCP candidate.updateStep callback is detected as an LCP candidate, and ultimately the later-appearing content is recorded as the LCP.Therefore, it's worth considering how to interpret LCP and other metric scores depending on what the website's measurement goals are.
This website has a transitional structure in which initial content appears temporarily and is then removed, and the actual main content appears late. In this structure, Lighthouse ends measurement before the substantive content appears, so LCP measures short, whereas the Performance tab measures up to and including the point the user actually sees the content, so LCP is recorded as longer.
This is not simple measurement error but an intended measurement difference stemming from the two tools' design purposes. Therefore, if you want performance metrics close to the actual user experience, it's advisable to judge based on the Performance tab's results or consider adopting RUM-based measurement.
Due to its structure where initial content appears with a delay, the current website reveals several limitations in user experience, performance metrics, and search optimization. This can be a strength in terms of deliberately staged visual effects, but it doesn't quite fit the purpose of a portfolio website operating in the real world.
This section proposes improvements based on the Progressive Enhancement principle that address the drawbacks of the current structure while still preserving the animation effects.
What is Progressive Enhancement? https://en.wikipedia.org/wiki/Progressive_enhancement
Progressive Enhancement is a design philosophy for ensuring web accessibility and reliability: core content and functionality are delivered first in their most basic form, and then visual and interactive features are added progressively depending on the browser's or device's capabilities and the user's environment.
In other words, content and semantic structure must always come first, and JavaScript or advanced styling should operate optionally, only after the basic functionality is guaranteed.
This principle is widely adopted as a foundational strategy for web development that accounts for diverse user environments, and it directly benefits performance optimization, accessibility, and SEO.
The most important improvement to consider first is including the main content in the initial HTML. In the current structure, the substantive content is mounted via JavaScript only after the intro animation completes, so browsers and performance measurement tools perceive the page as having no content.
This delayed-content-exposure structure not only worsens performance metrics like LCP, but can also lead to unfavorable SEO outcomes because search engines cannot properly index the content.
Content-first rendering here does not mean server-side rendering (SSR) or a framework migration. Of course, adopting SSR could improve performance even more dramatically. But even in the current CSR-based environment, this can be fully achieved by including meaningful content in the HTML and then revealing it visually through animation.
For example, if you include the main content in the DOM up front and implement the visual transition using CSS properties like opacity, transform, and visibility, users can perceive the information immediately upon entering the page, and performance measurement tools can reflect it accurately. As a result, metrics like LCP are measured against the substantive content, and the structure becomes SEO-friendly.
This approach is the most practical improvement strategy that keeps the current CSR structure while conforming to the Progressive Enhancement principle.
When applying animation, rather than delaying the rendering point of the content itself, it's preferable to give already-rendered content a progressive-reveal effect through visual styling.
For example, the current approach of creating and mounting specific DOM nodes after JavaScript executes causes browsers and performance measurement tools to recognize that content late, and metrics like LCP and FCP can measure unfavorably. In addition, when content is inserted late, search engine crawlers may fail to discover it.
To solve this, I suggest a visual animation approach using properties like these:
opacity: gradually increase opacity for a natural entrancetransform: movement or scale effects using translateY, scale, etc.clip-path: effects where an element is progressively clipped or revealed in a given shapevisibility: reveal by switching visibility: hidden โ visible<!-- The following example code was written independently of the actual portfolio. -->
<section class="intro">
<h1 class="intro-title">I'm Kim Seonghyun</h1>
<p class="intro-description">Frontend developer portfolio</p>
</section>
<style>
.intro {
opacity: 0;
transform: translateY(30px);
transition:
opacity 0.6s ease-out,
transform 0.6s ease-out;
}
.intro.visible {
opacity: 1;
transform: translateY(0);
}
</style>
<script>
window.addEventListener('DOMContentLoaded', () => {
requestAnimationFrame(() => {
document.querySelector('.intro')?.classList.add('visible')
})
})
</script>
In the example above, the content exists in the HTML from the start, and JavaScript merely adds a single class to apply the visual animation. Structured this way, search engines, Lighthouse, and the browser's rendering engine can all recognize the content accurately, while users still experience a smooth transition effect.
This approach is also built around animation properties that are optimized at the GPU level (opacity, transform), so it can operate reliably on mobile devices without performance degradation.
Given the nature of a portfolio, providing a visually striking presentation to first-time visitors can be important. For repeat visitors, however, content accessibility matters more. Accordingly, a strategy of branching whether to run the animation based on visit history could be effective.
// Run the full intro animation only for first-time visitors
const showFullAnimation = !localStorage.getItem('returning-visitor')
if (showFullAnimation) {
runIntroAnimation()
localStorage.setItem('returning-visitor', 'true')
} else {
skipToContentImmediately()
}
This approach can be a compromise that reduces user fatigue while maintaining visual impact on first exposure.
When applying the improvements suggested above to a real project, how about using the following items as a checklist to review up front whether there are structural issues in code quality, performance, and accessibility?
will-change property?will-change is a CSS property that hints to the browser that "this property is about to change." For example, setting will-change: transform lets the browser prepare layer separation or GPU-accelerated handling for that element in advance, which can reduce layout calculation and repaint costs. However, excessive use can actually degrade performance, so it's best to limit it to the key elements where changes are concentrated.prefers-reduced-motion media query?prefers-reduced-motion is a CSS media query that can detect when a user has explicitly indicated in their OS or browser that they prefer minimized animation. Using it, you can minimize or skip animations and transition effects out of consideration for motion-sensitive users.
@media (prefers-reduced-motion: reduce) {
.animated {
animation: none;
transition: none;
}
}
opacity and transform, and avoiding properties that trigger layout/paint/reflow (top, left, height, etc.).This checklist serves as a standard for evaluating not just whether the animation looks good, but web accessibility, performance stability, and maintainability as a whole. Especially for websites like portfolios that demand both content delivery and visual polish, these criteria become the very standard of quality.
Therefore, to provide users with an impressive experience while also satisfying discoverability and performance optimization, I believe a design that considers both content-first rendering and a Progressive Enhancement strategy must be at the core.
Beyond the core bottlenecks mentioned above, I also analyzed additional fine-grained optimization points that can contribute to overall performance.
The current bundle file, index-CF5N6APp.js, is about 138KB. By recent frontend standards this isn't excessively large, but considering that this is a static site with no complex logic, it's on the heavy side. In particular, this website is structured as a single-page application (SPA), so this bundle structure directly affects performance.
The current JavaScript file contains not only the developer's own service logic but also all of the framework and external library code: react, react-dom, emotion, gsap, and so on. Code splitting is also not applied, so every piece of logic that makes up the page depends on a single bundle. Because of this, the browser must download and parse the entire resource at once, and gains none of the benefits of parallel loading.
A JavaScript file over 100KB may not be fatal for a typical CSR-based service. But since this website has no SSR, the browser depends on JavaScript for the entire process of constructing the initial screen.
In this structure, the longer JS parsing and execution take, the more directly content rendering is delayed, which in turn negatively affects key performance metrics like LCP and TTI. This delay can be even more pronounced in the current setup, where the actual content appears after the intro animation.
In the current structure, all code lives in a single bundle, and major libraries like react, react-dom, emotion, and gsap are included in index.js as well. This structure causes the following problems:
To solve this, you need a strategy of splitting out only the code strictly needed for initial render into a minimal chunk, and separating common libraries into their own chunks. This can be expected to yield:
react, react-dom, etc. rarely change, so cache utilization is maximizedIn Vite, this is easy to apply via the rollupOptions.output.manualChunks setting:
// vite.config.ts example
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
react: ['react', 'react-dom'],
},
},
},
},
}
Next.js adopts the same strategy: it separates common libraries into a file called framework.js, configured so that unchanging resources are cached long-term.


As you can see, even simple chunk separation can improve initial rendering performance, network load, and deployment efficiency all at once, and for an SPA like this project it can be an especially effective strategy.
Of course, excessive code splitting can actually slow initial loading in large web applications with many routes. But this site is an SPA composed of a single view without clear page boundaries, so simple library-level chunk separation alone can yield performance benefits.
In conclusion, the current structure loads unnecessarily large amounts of JavaScript all at once at initial render, and bundle optimization via chunk separation is an essential task to improve this. In a Vite-based build environment this is relatively simple to implement, so it should improve both performance and user experience in the actual deployed environment.
Based on the structure of https://portfolio-amber-mu-57.vercel.app/, we've taken a concrete look at what improvements are possible in terms of user experience and performance.
Despite being in an early stage of development, the current build is an impressive portfolio with technical polish, built on intense animation staging with GSAP, Emotion-based styling, and a modern frontend composition based on React and Vite. In particular, the UI/UX flow, centered on visual impact, clearly conveys an intent to showcase staging and expressiveness themselves rather than content.
That said, the structure where content mounts after the intro, and the current approach of handling every resource in a single bundle, can produce evaluation results in tools like Lighthouse that diverge from the actual user experience, and there appears to be room for improvement in search optimization and accessibility as well.
Since the portfolio is at an early stage, light structural improvements alone โ rendering order, code-splitting strategy, applying the Progressive Enhancement principle โ can deliver substantial performance gains without complex restructuring. And as content is added or the number of pages grows in the future, I believe the directions presented in this analysis can help you secure structural scalability and technical sustainability.
I sincerely hope this portfolio continues to grow, that the content and message within it reach a wider audience, and that you take a successful first step as a frontend developer.
If you have any questions or anything you'd like to discuss further, please feel free to reach out anytime. Thank you for reading!