Esigenza Reale

Ridurre il Time To First Byte (TTFB) servendo parti comuni del layout istantaneamente.

Analisi Tecnica

Problema: Il motore di template ri-elabora frammenti identici (es. header/footer) per ogni singola richiesta HTTP.

Perché: Implemento il caching dei frammenti. Ho scelto un approccio basato su cache distribuita per garantire che il lavoro di rendering venga fatto una sola volta per tutti i nodi del cluster.

Esempio Implementativo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/* Strategia 1: caching a livello di TemplateResolver (frammenti completamente
    statici). */
@Bean public SpringResourceTemplateResolver templateResolver() {
    SpringResourceTemplateResolver resolver = new
        SpringResourceTemplateResolver();
    resolver.setPrefix("classpath:/templates/");
    resolver.setSuffix(".html");
    resolver.setCacheable(true);
    resolver.setCacheTTLMs(3_600_000L);
    // Cache di 1 ora: per header/footer che cambiano raramente
    return resolver;
}
/* Strategia 2: caching del frammento HTML già renderizzato con Spring Cache +
    Redis. Uso questa strategia per frammenti semi-dinamici (es. menu che
    dipende dal ruolo ma non cambia per ogni richiesta). */
@Service public class FragmentCacheService {
    private final TemplateEngine templateEngine;
    private final ApplicationContext applicationContext;
    @Cacheable( value = "rendered-fragments", key = "#fragmentName + ':' +
        #cacheKey", unless = "#result == null" ) public String
        renderFragment(String fragmentName, String cacheKey, Map<String, Object>
        variables) {
        // Creo un contesto Thymeleaf con le variabili fornite Context context =
            new Context()
        ;
        context.setVariables(variables);
        // Processo solo il frammento specificato, non l'intero template
        return templateEngine.process(fragmentName, context);
    }
    @CacheEvict(value = "rendered-fragments", allEntries = true) public void
        invalidateAllFragments() {
        // Invalido quando i dati di navigazione cambiano (es. nuova voce di
            menu aggiunta)
    }
}
/* Configurazione Redis per la cache distribuita: */
@Configuration
@EnableCaching public class CacheConfig {
    @Bean public RedisCacheManager cacheManager(RedisConnectionFactory factory)
        {
        RedisCacheConfiguration fragmentConfig =
            RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofHours(1))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
            .fromSerializer(new StringRedisSerializer()));
        return RedisCacheManager.builder(factory)
            .withCacheConfiguration("rendered-fragments", fragmentConfig)
            .build();
    }
}
/* Controller che usa il fragment caching: */
@Controller public class PageController {
    @Autowired private FragmentCacheService fragmentCache;
    @GetMapping("/dashboard") public String dashboard(Model model,
        Authentication auth) {
        // Il navbar viene renderizzato una sola volta per ruolo e cachato in
            Redis String navbarHtml = fragmentCache.renderFragment(
            "fragments/navbar", // Template del frammento
            auth.getAuthorities().toString(), // Chiave di cache basata sul
            ruolo Map.of("user", auth.getName(), "roles", auth.getAuthorities())
            )
        ;
        model.addAttribute("navbarHtml", navbarHtml);
        model.addAttribute("data", dashboardService.getData());
        return "dashboard";
    }
}
/* Nel template dashboard.html: inietto l'HTML pre-renderizzato senza
    re-processing. */
// <div th:utext="$
{
    navbarHtml
}
"></div> <!-- th:utext per HTML raw non escaped -->
// <div th:replace="~
{
    fragments/dashboard-content :: content
}
"></div>
/* In application.properties: */
// spring.thymeleaf.cache=true # Obbligatorio in produzione //
    spring.cache.type=redis // spring.data.redis.host=redis-cluster.internal //
    spring.data.redis.port=6379