En la Guia de arquitectura Shopify cubrimos los fundamentos. Hoy nos ocupamos de un hecho que muchos ignoran: El rendimiento es arquitectura.
En smplx. trabajamos con J.Clay y logramos un aumento de ingresos del +107% a traves de optimizaciones de rendimiento. Eso no fue suerte -- fue trabajo de arquitectura sistematico.
Queremos compartir esta leccion contigo.
Por que el rendimiento es arquitectura
La tesis: Las tiendas rapidas no son el resultado de servidores rapidos. Son el resultado de decisiones de arquitectura bien pensadas.
Los numeros:
- Cada 100ms de retraso = 0,7% menos conversiones (Google)
- El 53% abandona las tiendas si tardan mas de 3 segundos
- Los usuarios moviles son 2x mas sensibles al rendimiento lento
Pero aqui esta la verdad mas importante: Estos numeros no provienen de servidores mas rapidos. Provienen de:
- Lo que cargas
- Cuando lo cargas
- Si el navegador puede cachearlo
Eso es arquitectura, no DevOps.
Core Web Vitals: Las metricas que importan
Google mide tres metricas:
1. LCP (Largest Contentful Paint)
Que es? Cuando esta el contenido visible mas grande cargado y visible?
Benchmark:
- Bueno: < 2,5s
- Aceptable: 2,5-4,0s
- Malo: > 4,0s
Que afecta al LCP?
- Imagenes (la gran grafica del hero)
- Texto (si no esta embebido en el HTML)
- Videos
- Imagenes de fondo con CSS
2. FID (First Input Delay) / INP (Interaction to Next Paint)
Que es? Cuanto tarda el navegador en responder a tu interaccion?
Benchmark:
- Bueno: < 100ms
- Aceptable: 100-300ms
- Malo: > 300ms
Que afecta al INP?
- Ejecucion de JavaScript (demasiado JS bloquea el hilo principal)
- Long Tasks (mas de 50ms de JavaScript sin pausa)
- Event listeners ausentes
3. CLS (Cumulative Layout Shift)
Que es? Tu pagina salta durante la carga?
Benchmark:
- Bueno: < 0,1
- Aceptable: 0,1-0,25
- Malo: > 0,25
Que afecta al CLS?
- Imagenes sin width/height
- Fuentes que cambian
- Anuncios que aparecen
- Banners de cookies
Critical Rendering Path: La base
Que es? El orden en el que el navegador renderiza tu pagina.
1. Parsear HTML
2. Parsear CSS (bloquea el renderizado!)
3. Ejecutar JavaScript (bloquea el renderizado!)
4. Renderizar primeros pixeles (inicio de LCP)
5. Volverse interactivo (INP relevante)
6. Todo cargado (fin de LCP)
La decision de arquitectura: Minimizar el bloqueo.
Liquid y Critical CSS
<!-- MAL: Todo se bloquea -->
<link rel="stylesheet" href="/cdn/all.css">
<script src="/cdn/all.js"></script>
<!-- BIEN: Critico inline, resto async -->
<style>
/* Solo Critical CSS: Header, Hero, Above-Fold */
.header { ... }
.hero { ... }
.button { ... }
</style>
<link rel="stylesheet" href="/cdn/non-critical.css" media="print" onload="this.media='all'">
<script defer src="/cdn/main.js"></script>
<script async src="/cdn/analytics.js"></script>
Que hace esto:
- El CSS critico no bloquea
- El CSS no critico se carga en segundo plano
- JavaScript se carga con
defer(despues del parseo HTML) - Analytics con
async(nada critico)
Estrategia de imagenes: La mayor palanca de rendimiento
La realidad: Las imagenes son el 60-80% del ancho de banda en tiendas Shopify.
Imagenes responsive con Picture
<!-- MAL: Un tamano para todos -->
<img src="{{ product.featured_image.src }}" alt="{{ product.featured_image.alt }}">
<!-- BIEN: WebP, multiples tamanos -->
<picture>
<source
type="image/webp"
srcset="{{ product.featured_image | img_url: '300x' }} 300w,
{{ product.featured_image | img_url: '600x' }} 600w,
{{ product.featured_image | img_url: '900x' }} 900w"
sizes="(max-width: 768px) 100vw, 50vw">
<img
src="{{ product.featured_image | img_url: '600x' }}"
alt="{{ product.featured_image.alt }}"
width="600"
height="400"
loading="lazy"
decoding="async">
</picture>
Que logra esto:
- WebP para navegadores que lo soportan
- Tamanos responsive basados en el dispositivo
loading="lazy"= lo que no esta en el viewport se carga despuesdecoding="async"= no bloquea el hilo principal
API de entrega de imagenes de Shopify
<!-- Con la transformacion nativa de Shopify -->
{{ product.featured_image | image_url: width: 600 }}
Shopify entrega imagenes desde el CDN con transformaciones automaticas. Esto es mejor que los optimizadores de imagenes externos.
Estrategia de fuentes: A menudo olvidada
Las fuentes suelen ser >50KB. Pueden bloquear el LCP!
<!-- MAL: Google Fonts bloquea -->
<link href="https://fonts.googleapis.com/css2?family=Inter" rel="stylesheet">
<!-- BIEN: Preload + Swap -->
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet">
O aun mejor: Aloja tus fuentes tu mismo.
<!-- AUN MEJOR: Self-Hosted -->
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap;
}
Por que font-display: swap? El navegador muestra la fuente de respaldo inmediatamente, luego la intercambia. Sin fase de LCP en blanco.
Scripts de terceros: El centro de costes oculto
La realidad: Una tienda Shopify promedio carga 30+ scripts externos.
Eso no esta bien. Cada script externo:
- Necesita una busqueda DNS
- Necesita una solicitud HTTP
- Carga JavaScript extra
- Compite con tu JavaScript
Realizar una auditoria de scripts
// En la consola del navegador
performance.getEntriesByType('resource')
.filter(r => r.initiatorType === 'script')
.forEach(r => {
console.log(r.name, Math.round(r.duration) + 'ms', r.transferSize);
});
Que sale?
- Google Analytics: ~200ms
- Facebook Pixel: ~300ms
- Attentive (Popups): ~400ms
- Custom Font Loader: ~150ms
- ...
- Total: 3+ segundos solo para terceros!
Arquitectura de scripts
<!-- MAL: Todo inmediatamente -->
<script src="analytics.js"></script>
<script src="pixel.js"></script>
<script src="popup.js"></script>
<script src="chat.js"></script>
<!-- BIEN: Lazy Loading -->
<script>
// Analytics inmediatamente (es importante)
// Pero con async
const script = document.createElement('script');
script.src = '/analytics.js';
script.async = true;
document.head.appendChild(script);
// Popup/Chat: Solo cuando es interactivo
window.addEventListener('DOMContentLoaded', () => {
loadScript('/popup.js');
});
// Terceros despues de 5 segundos
setTimeout(() => {
loadScript('/pixel.js');
}, 5000);
</script>
Eso es arquitectura: Que es critico? Que puede esperar?
Caching: La palanca subestimada
HTTP Caching para assets del theme
<!-- En theme liquid (p. ej., theme.liquid) -->
<link rel="stylesheet" href="{{ 'theme.css' | asset_url }}" media="all">
Shopify sirve assets con headers de cache largos. Eso esta bien. Pero:
- Imagenes: Cambian a menudo > cache corto
- CSS: Cambia raramente > cache largo
- JavaScript: Cambia regularmente > cache medio
Browser Caching con Liquid
{% if request.host == 'shop.example.com' %}
<!-- Ya cacheado, puede ser agresivo -->
<img src="{{ product.image | img_url: '300x' }}" loading="lazy">
{% endif %}
Service Worker para offline
// Si tienes una tienda headless (Next.js/Remix)
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
// En el Service Worker
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) => {
return cache.addAll([
'/',
'/products',
'/collections'
]);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => response || fetch(event.request))
);
});
Esto permite navegacion offline y cargas mas rapidas a traves del caching.
Code Splitting: Solo cargar lo necesario
Liquid con JavaScript condicional
{% if page.handle == 'contact' %}
<script src="{{ 'form-validation.js' | asset_url }}"></script>
{% endif %}
{% if template == 'product' %}
<script src="{{ 'product-gallery.js' | asset_url }}"></script>
{% endif %}
Headless (Next.js) con Dynamic Imports
// components/ProductGallery.jsx
import dynamic from 'next/dynamic';
const GalleryComponent = dynamic(
() => import('./gallery-component'),
{ loading: () => <p>Cargando...</p> }
);
export default function ProductPage() {
return (
<>
<ProductInfo />
<GalleryComponent />
</>
);
}
El navegador solo carga gallery-component en la pagina de producto.
Lazy Loading de secciones: Solo renderizar Above-Fold
En Liquid/Theme tradicional
<!-- Above-Fold: Renderizar -->
<section class="hero">
...
</section>
<!-- Below-Fold: Lazy-Load -->
<div
class="lazy-section"
data-src="{% section 'product-carousel' %}">
Carga despues...
</div>
<script>
// Intersection Observer: Solo cargar cuando es visible
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const section = entry.target;
const html = document.querySelector(`[data-src="${section.dataset.src}"]`).dataset.src;
section.innerHTML = html;
observer.unobserve(section);
}
});
});
document.querySelectorAll('.lazy-section').forEach(section => {
observer.observe(section);
});
</script>
Efecto: Una pagina de 10 secciones se carga inicialmente con solo 2-3 secciones.
Caso de estudio: J.Clay [+107% de ingresos]
J.Clay necesitaba velocidad. Esto es lo que hicimos:
Antes
- LCP: 3,8s
- FID: 280ms
- CLS: 0,15
- Conversion: 2,1%
Cambios de arquitectura
Estrategia de imagenes (mayor impacto)
- WebP + Tamanos responsive
- Lazy Loading para below-fold
- Resultado: -1,2s LCP
Limpieza de terceros
- Pixel eliminado (no ROI-positivo)
- Analytics con
async - Script de popup cargado de forma lazy
- Resultado: -0,6s LCP, -150ms FID
Critical CSS Inline
- Header + Hero + CTA inline
- Resto con truco de media="print"
- Resultado: -0,4s LCP
Code Splitting
- Script de flujo de compra solo en checkout
- Galeria de producto solo en paginas de producto
- Resultado: -100ms INP
Despues
- LCP: 1,1s
- FID: 85ms
- CLS: 0,08
- Conversion: 4,3% (+107%!)
Eso no fueron "servidores mas rapidos". Fue arquitectura.
Monitoreo de rendimiento: No construir y olvidar
Tracking basado en Liquid
<script>
window.addEventListener('load', () => {
const metrics = {
lcp: performance.getEntriesByName('largest-contentful-paint')[0]?.renderTime,
fid: performance.getEntriesByType('first-input')[0]?.processingStart,
cls: Array.from(document.querySelectorAll('[style*="transform"]')).length
};
// Enviar a analytics
fetch('/api/metrics', {
method: 'POST',
body: JSON.stringify(metrics)
});
});
</script>
Con la libreria Web Vitals (Recomendado)
import { getLCP, getFID, getCLS } from 'web-vitals';
getLCP(console.log);
getFID(console.log);
getCLS(console.log);
Checklist: Revision de arquitectura de rendimiento
Usa esto para tu proximo proyecto:
- LCP por debajo de 2,5s (Lighthouse)
- Todas las imagenes responsive + WebP
- Ninguna imagen sin width/height
- CSS critico inline, resto diferido
- Fuentes con
font-display: swap - Scripts de terceros solo cuando sea necesario, async/defer
- Code splitting por template/pagina
- Service Worker para offline (si es headless)
- Lazy loading para secciones below-fold
- Headers de caching configurados
- Monitoreo de Web Vitals activo
- Lighthouse Score > 85
Sobre el autor
Claudio Gerlich es Technical Architect en smplx. y esta especializado en Shopify desde 2020. Con J.Clay hemos demostrado que un aumento de ingresos del +100% a traves de una arquitectura de rendimiento bien pensada es posible.
El rendimiento no es suerte. Son decisiones. Buenas decisiones.
smplx. es Shopify Technical Partner (desde 2020) con sede en Munsterland, NRW.