← 개발일지

Fix Slow Login Pages in Thymeleaf by Splitting Your Layout


A Monitoring Alert That Exposed a Structural Problem

A web monitoring service flagged a login page with a 30-second navigation timeout. The server was healthy. Loading the page directly took under 7 seconds. It was a one-off alert — but investigating it revealed a problem baked into the project structure itself.

The Problem: A Login Page Loading an Entire E-Commerce Site

The project uses Thymeleaf Layout Dialect, where every page inherits from a single shared layout. This layout pulls in header, footer, and resource fragments containing all the CSS, JS, and service calls the shopping pages need.

The login page inherited the same layout. That meant two expensive things happened on every single login page request:

Server side: The header fragment declared 15+ service method calls in a th:with block — cart counts, banners, marketing menus, point balances, budget info, approval counts. None of these matter on a login page, especially for unauthenticated users. But th:with evaluates all SpEL expressions eagerly at declaration time, regardless of whether the template actually uses them.

Client side: The shared resource fragment loaded an Excel library, a slider library, animation CSS, and more — over 40 HTTP requests totaling roughly 1MB. For a page that consists of two input fields and one AJAX call.

The th:with Trap

This is the core of the issue:

<th:block th:with="a = ${@serviceA.query()},
                   b = ${@serviceB.query()},
                   c = ${@serviceC.query()}">
    <!-- Only 'a' is used in the template -->
    <span th:text="${a}"></span>
    <!-- But serviceB.query() and serviceC.query() already ran -->
</th:block>

When this pattern sits inside a page-specific template, it's that page's problem alone. But when it lives in a shared layout fragment, every page inheriting the layout pays the full cost. A header with 10 service calls shared across 100 pages means 1,000 unnecessary DB round-trips per user session.

Under normal load, those calls finish fast enough. But a momentary spike in DB connection pool usage or a slow external CDN turns a 7-second page into a 30-second timeout.

The Fix: A Dedicated Minimal Layout

The approach is straightforward: create a stripped-down layout that excludes header, footer, and navigation fragments entirely.

Conceptually:

Before:
  login.html → default_layout → header (15+ DB calls) + resources (40+ files) + footer

After:
  login.html → login_layout → minimal CSS/JS only (~10 files)
  all other pages → default_layout → unchanged

The minimal layout includes only the base stylesheet and jQuery — no Excel libraries, no sliders, no animation CSS. Since it doesn't include the header fragment at all, the 15+ service calls in th:with never execute.

The change in the login template is a single line:

<!-- Before -->
layout:decorate="~{layout/default_layout}"

<!-- After -->
layout:decorate="~{layout/login_layout}"

This one change eliminates 15+ server-side DB calls per request and drops client-side HTTP requests from 40+ to under 10.

When to Split Your Layout

The tradeoff is an extra layout file to maintain. Shared changes may need to be applied in two places instead of one.

But loading 15 DB queries and 1MB of assets on a login page — just because it's convenient to share one layout — is a clear waste. The decision rule is simple: does this page actually use the header, footer, and navigation? If not, it's a candidate for a separate layout.

Beyond login, good candidates include error pages, terms of service, password reset, and standalone landing pages.

Testing is lightweight since layout separation only changes the template layer. Verify the page's core functionality — form submission, validation, internal navigation, external links — and you're done. Business logic is completely unaffected.