Luma – domyślny motyw Magento 2 – ma poważny problem z wydajnością. Dziesiątki plików JS, jQuery, RequireJS, knockout.js i plik CSS liczący setki KB. Hyvä (wymawiane „hiuva”, po fińsku „dobry”) to alternatywny motyw który wyrzuca ten stack i zastępuje go Alpine.js i Tailwind CSS. Wyniki PageSpeed skaczą z 30-40 do 90+. Pokazuję architekturę i jak zacząć.
Co Hyvä zmienia w stosunku do Luma
| Aspekt | Luma | Hyvä |
|---|---|---|
| JS Framework | RequireJS + jQuery + knockout.js | Alpine.js (13KB gzip) |
| CSS Framework | LESS (kompilowany) | Tailwind CSS (purge CSS = mały bundle) |
| Szablony | PHTML + .html knockout | PHTML + Alpine.js directives inline |
| PageSpeed (typowy) | 30-50 | 85-95 |
| Core Web Vitals LCP | Często powyżej 4s | Często poniżej 2.5s |
| Checkout | Wbudowany (knockout.js) | Hyvä Checkout (osobny moduł, płatny) |
| Licencja | Open Source | Płatna (jednorazowo ~1000 EUR) |
Instalacja
# Hyvä wymaga dostępu do prywatnego repo po zakupie licencji # Dodaj repozytorium do composer.json composer config repositories.hyva-themes composer https://hyva-themes.repo.packagist.com/YOUR_TOKEN/ # Instalacja composer require hyva-themes/magento2-default-theme composer require hyva-themes/magento2-theme-module # Utwórz własny child theme mkdir -p app/design/frontend/Vendor/MyTheme
<!-- app/design/frontend/Vendor/MyTheme/theme.xml -->
<?xml version="1.0"?>
<theme xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Config/etc/theme.xsd">
<title>Vendor MyTheme</title>
<parent>Hyva/default</parent>
</theme>
// app/design/frontend/Vendor/MyTheme/web/tailwind/tailwind.config.js
module.exports = {
content: [
// Skanuj PHTML i HTML pod kątem klas Tailwind
'../**/*.phtml',
'../../../../../../vendor/hyva-themes/magento2-default-theme/**/*.phtml',
'./src/**/*.js',
],
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#1a56db',
lighter: '#3f83f8',
darker: '#1e3a5f',
},
secondary: {
DEFAULT: '#ff6b35',
},
},
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
},
},
plugins: [],
}
Szablony PHTML z Alpine.js
W Hyvä każdy komponent interaktywny to Alpine.js zamiast knockout.js. Różnica jest ogromna w czytelności:
<?php
// app/design/frontend/Vendor/MyTheme/Magento_Catalog/templates/product/view/addtocart.phtml
/** @var \Magento\Catalog\Block\Product\View $block */
/** @var \Magento\Framework\Escaper $escaper */
$product = $block->getProduct();
$productId = $product->getId();
$productName = $escaper->escapeHtml($product->getName());
$maxQty = (int) ($product->getExtensionAttributes()->getStockItem()->getMaxSaleQty() ?? 999);
?>
<div
x-data="initAddToCart()"
x-init="initProduct()"
class="product-add-to-cart"
>
<!-- Selector ilości z Alpine.js - bez knockout, bez RequireJS -->
<div class="flex items-center gap-4 mb-4">
<button
@click="qty = Math.max(1, qty - 1)"
:disabled="qty <= 1"
class="w-10 h-10 border border-gray-300 rounded flex items-center justify-center
hover:bg-gray-100 disabled:opacity-50"
>
-
</button>
<input
type="number"
x-model.number="qty"
min="1"
:max="maxQty"
class="w-16 text-center border border-gray-300 rounded h-10"
/>
<button
@click="qty = Math.min(maxQty, qty + 1)"
:disabled="qty >= maxQty"
class="w-10 h-10 border border-gray-300 rounded flex items-center justify-center
hover:bg-gray-100 disabled:opacity-50"
>
+
</button>
</div>
<!-- Przycisk "Dodaj do koszyka" -->
<button
@click="addToCart()"
:disabled="isLoading"
class="w-full bg-primary hover:bg-primary-darker text-white font-semibold
py-3 px-6 rounded transition-colors duration-200
disabled:opacity-50 disabled:cursor-not-allowed"
>
<span x-show="!isLoading">
<?= $escaper->escapeHtml(__('Add to Cart')) ?>
</span>
<span x-show="isLoading" class="flex items-center justify-center gap-2">
<svg class="animate-spin w-5 h-5" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.4 0 0 5.4 0 12h4z"></path>
</svg>
<?= $escaper->escapeHtml(__('Adding...')) ?>
</span>
</button>
<!-- Komunikaty sukcesu/błędu -->
<div
x-show="message"
x-transition
:class="messageType === 'success' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'"
class="mt-3 p-3 rounded text-sm"
x-text="message"
></div>
</div>
<script>
function initAddToCart() {
return {
qty: 1,
maxQty: <?= (int) $maxQty ?>,
productId: <?= (int) $productId ?>,
isLoading: false,
message: '',
messageType: 'success',
initProduct() {
// Nasłuchuj na zmiany opcji produktu (warianty)
window.addEventListener('configurable-selection-changed', (e) => {
if (e.detail.productId !== this.productId) return;
this.maxQty = e.detail.maxQty || 999;
});
},
async addToCart() {
this.isLoading = true;
this.message = '';
try {
const response = await fetch('/checkout/cart/add', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
product: this.productId,
qty: this.qty,
form_key: hyva.getFormKey(),
}),
});
const data = await response.json();
if (data.success) {
this.message = '<?= $escaper->escapeHtml(__("Product added to cart")) ?>';
this.messageType = 'success';
// Aktualizuj licznik koszyka przez event
window.dispatchEvent(new CustomEvent('reload-customer-section-data'));
} else {
this.message = data.error || '<?= $escaper->escapeHtml(__("Could not add product")) ?>';
this.messageType = 'error';
}
} catch (error) {
this.message = '<?= $escaper->escapeHtml(__("An error occurred")) ?>';
this.messageType = 'error';
} finally {
this.isLoading = false;
}
}
};
}
</script>
Eventy w Hyvä – zamiast pubsub knockout
Hyvä używa natywnych DOM events zamiast knockout pubsub. To prostsze i bardziej przewidywalne:
// Wysyłanie eventu (np. po dodaniu do koszyka)
window.dispatchEvent(new CustomEvent('reload-customer-section-data'));
window.dispatchEvent(new CustomEvent('cart-updated', {
detail: { itemCount: 3, total: '99.99' }
}));
// Nasłuchiwanie (np. licznik koszyka w headerze)
// x-data w template PHTML
{
cartCount: 0,
init() {
window.addEventListener('cart-updated', (event) => {
this.cartCount = event.detail.itemCount;
});
// Załaduj dane koszyka przy inicjalizacji
window.addEventListener('private-content-loaded', (event) => {
const cart = event.detail.data.cart;
this.cartCount = cart?.summary_count || 0;
});
}
}
Moduły zewnętrzne a kompatybilność z Hyvä
Główne wyzwanie przy wdrożeniu Hyvä to kompatybilność modułów zewnętrznych – każdy który ma własne szablony PHTML z knockout.js wymaga przepisania lub użycia trybu kompatybilności:
# Sprawdź które zainstalowane moduły mają szablony kompatybilne z Hyvä # Społeczność utrzymuje listę: https://www.hyva-themes.com/magento-2-hyvae-compatibility # Moduł Hyvä Compatibility - fallback dla modułów bez natywnego wsparcia # (używa iframe dla komponentów knockout - działa ale wolniej niż natywne Hyvä) composer require hyva-themes/magento2-compat-module-fallback
Podsumowanie
Hyvä rozwiązuje realny problem wydajności frontendu Magento 2. Zamiana RequireJS + knockout.js na Alpine.js + Tailwind to nie tylko lepsza wydajność – to też prostszy, bardziej czytelny kod szablonów. Główna bariera to koszt licencji i konieczność przepisania lub sprawdzenia kompatybilności każdego modułu zewnętrznego. Dla nowych projektów Magento 2.4.x warto rozważyć Hyvä jako domyślny wybór frontendu zamiast Lumy.
