Web Components คืออะไร
Web Components เป็น web standard ที่ให้สร้าง reusable custom HTML elements ด้วย native browser APIs ไม่ต้องพึ่ง framework เช่น React หรือ Vue ประกอบด้วย 3 technologies หลัก Custom Elements กำหนด HTML elements ใหม่ด้วย JavaScript classes, Shadow DOM encapsulate styles และ markup ไม่กระทบส่วนอื่นของ page และ HTML Templates กำหนด markup ที่ reuse ได้ด้วย template และ slot elements
ข้อดีของ Web Components ได้แก่ Framework Agnostic ใช้ได้กับทุก framework หรือไม่มี framework, Native Browser Support ทุก modern browsers รองรับ, Encapsulation Shadow DOM ป้องกัน CSS conflicts, Reusability ใช้ component เดียวกันได้ทุก project, Interoperability ทำงานร่วมกับ React, Vue, Angular ได้ และ Performance ไม่มี framework overhead
Edge Computing สำหรับ Web Components หมายถึงการ render และ serve components ที่ edge servers ใกล้ผู้ใช้ ลด latency และเพิ่ม performance โดยเฉพาะ Server-Side Rendering (SSR) ที่ edge, streaming HTML, component-level caching
สร้าง Web Components ตั้งแต่ต้น
เขียน Web Components ด้วย vanilla JavaScript
// === Web Components Examples ===
// 1. Basic Custom Element
class ProductCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
static get observedAttributes() {
return ['name', 'price', 'image', 'currency'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
}
}
connectedCallback() {
this.render();
}
render() {
const name = this.getAttribute('name') || 'Product';
const price = this.getAttribute('price') || '0';
const image = this.getAttribute('image') || '';
const currency = this.getAttribute('currency') || 'THB';
this.shadowRoot.innerHTML = `
" alt="">` : 'No Image'}
`;
this.shadowRoot.querySelector('button').addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('add-to-cart', {
detail: { name, price, currency },
bubbles: true,
composed: true,
}));
});
}
}
customElements.define('product-card', ProductCard);
// Usage:
//
// 2. Data Fetching Component
class DataTable extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._data = [];
}
static get observedAttributes() {
return ['src', 'columns'];
}
async connectedCallback() {
const src = this.getAttribute('src');
if (src) {
try {
const response = await fetch(src);
this._data = await response.json();
} catch (e) {
this._data = [];
}
}
this.render();
}
render() {
const columns = (this.getAttribute('columns') || '').split(',').filter(Boolean);
this.shadowRoot.innerHTML = `
`).join('')}
`).join('')}
`).join('')}
`;
}
}
customElements.define('data-table', DataTable);
console.log("Web Components defined");
Edge Computing สำหรับ Web Components
Serve Web Components จาก edge
// === Edge-Rendered Web Components ===
// 1. Cloudflare Worker — SSR Web Components at Edge
// ===================================
// export default {
// async fetch(request, env) {
// const url = new URL(request.url);
//
// // Get user context from edge
// const country = request.cf?.country || 'TH';
// const city = request.cf?.city || 'Bangkok';
//
// // Render component at edge
// const productHtml = renderProductCard({
// name: 'MacBook Pro M4',
// price: getLocalPrice(79900, country),
// currency: getCurrency(country),
// image: '/images/macbook.webp',
// });
//
// // Build full page with edge-rendered components
// const html = `
//
//
//
// Web Components Edge Computing — สร้าง Custom | SiamCafe
//
//
//
//
// //
// `;
//
// return new Response(html, {
// headers: {
// 'Content-Type': 'text/html',
// 'Cache-Control': 'public, max-age=300',
// 'X-Rendered-At': 'edge',
// },
// });
// },
// };
// 2. Declarative Shadow DOM (SSR)
// ===================================
function renderProductCard(props) {
return `
`;
}
// 3. Component-Level Caching at Edge
// ===================================
// const componentCache = new Map();
//
// async function getCachedComponent(key, renderFn, ttlSeconds = 300) {
// const cached = componentCache.get(key);
// if (cached && Date.now() < cached.expiresAt) {
// return cached.html;
// }
//
// const html = await renderFn();
// componentCache.set(key, {
// html,
// expiresAt: Date.now() + ttlSeconds * 1000,
// });
//
// return html;
// }
//
// // Cache product card per country
// const html = await getCachedComponent(
// `product--`,
// () => renderProductCard({ ...product, price: getLocalPrice(product.price, country) }),
// 600 // 10 minutes
// );
// 4. Streaming HTML with Web Components
// ===================================
// async function streamPage(request) {
// const { readable, writable } = new TransformStream();
// const writer = writable.getWriter();
// const encoder = new TextEncoder();
//
// // Stream header immediately
// writer.write(encoder.encode(''));
// writer.write(encoder.encode(''));
// writer.write(encoder.encode(''));
//
// // Stream components as they're ready
// const products = await fetchProducts();
// for (const product of products) {
// const html = renderProductCard(product);
// writer.write(encoder.encode(html));
// }
//
// writer.write(encoder.encode('
'));
// writer.close();
//
// return new Response(readable, {
// headers: { 'Content-Type': 'text/html' },
// });
// }
function getCurrency(country) {
const map = { TH: 'THB', US: 'USD', JP: 'JPY', GB: 'GBP' };
return map[country] || 'USD';
}
function getLocalPrice(thbPrice, country) {
const rates = { TH: 1, US: 0.028, JP: 4.2, GB: 0.022 };
return Math.round(thbPrice * (rates[country] || 1));
}
console.log("Edge components configured");
Server-Side Rendering ที่ Edge
SSR patterns สำหรับ Web Components
#!/usr/bin/env python3
# ssr_engine.py — Web Component SSR Engine
import json
import logging
import html as html_lib
from datetime import datetime
from typing import Dict, List
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("ssr")
class WebComponentSSR:
def __init__(self):
self.components = {}
self.render_cache = {}
def register_component(self, tag_name, render_fn):
self.components[tag_name] = render_fn
def render(self, tag_name, props, use_dsd=True):
render_fn = self.components.get(tag_name)
if not render_fn:
return f"<{tag_name}>{tag_name}>"
inner_html = render_fn(props)
if use_dsd:
# Declarative Shadow DOM
return f"""<{tag_name} {self._props_to_attrs(props)}>
{inner_html}
{tag_name}>"""
else:
return f"<{tag_name} {self._props_to_attrs(props)}>{inner_html}{tag_name}>"
def render_page(self, components_list, title="Page"):
body_parts = []
for comp in components_list:
rendered = self.render(comp["tag"], comp["props"])
body_parts.append(rendered)
return f"""
Web Components Edge Computing — สร้าง Custom | SiamCafe
{"".join(body_parts)}
"""
def _props_to_attrs(self, props):
attrs = []
for key, value in props.items():
safe_value = html_lib.escape(str(value))
attrs.append(f'{key}="{safe_value}"')
return " ".join(attrs)
# Register components
ssr = WebComponentSSR()
def render_hero(props):
return f"""
{html_lib.escape(props.get('subtitle', ''))}
"""
def render_product_grid(props):
products = props.get("products", [])
cards = ""
for p in products:
cards += f"""
{html_lib.escape(p['name'])}
{p['price']:,} {p.get('currency', 'THB')}
"""
return f"""
{cards}"""
ssr.register_component("hero-section", render_hero)
ssr.register_component("product-grid", render_product_grid)
# Render page
page = ssr.render_page([
{"tag": "hero-section", "props": {"title": "Web Components Shop", "subtitle": "Built with Edge SSR"}},
{"tag": "product-grid", "props": {"products": [
{"name": "MacBook Pro", "price": 79900, "currency": "THB"},
{"name": "iPhone 16", "price": 39900, "currency": "THB"},
{"name": "iPad Air", "price": 24900, "currency": "THB"},
]}},
], title="Edge Shop")
print(f"Rendered page: {len(page)} chars")
Performance Optimization
เพิ่ม performance สำหรับ Web Components
// === Web Components Performance ===
// 1. Lazy Loading Components
class LazyComponent extends HTMLElement {
constructor() {
super();
this._loaded = false;
}
connectedCallback() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !this._loaded) {
this._loaded = true;
this.loadComponent();
observer.unobserve(this);
}
});
}, { rootMargin: '200px' });
observer.observe(this);
// Show placeholder
this.innerHTML = '';
}
async loadComponent() {
const src = this.getAttribute('component-src');
if (src) {
await import(src);
}
// Re-render with actual component
const tag = this.getAttribute('component-tag');
if (tag) {
const el = document.createElement(tag);
// Copy attributes
for (const attr of this.attributes) {
if (!attr.name.startsWith('component-')) {
el.setAttribute(attr.name, attr.value);
}
}
this.replaceWith(el);
}
}
}
customElements.define('lazy-component', LazyComponent);
// 2. Virtual List for Large Datasets
class VirtualList extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._items = [];
this._itemHeight = 50;
this._visibleCount = 20;
}
set items(data) {
this._items = data;
this.render();
}
connectedCallback() {
this._itemHeight = parseInt(this.getAttribute('item-height') || '50');
this.render();
this.shadowRoot.querySelector('.viewport').addEventListener('scroll', () => {
this.updateVisibleItems();
});
}
render() {
const totalHeight = this._items.length * this._itemHeight;
this.shadowRoot.innerHTML = `
`;
this.updateVisibleItems();
}
updateVisibleItems() {
const viewport = this.shadowRoot.querySelector('.viewport');
const itemsContainer = this.shadowRoot.querySelector('.items');
if (!viewport || !itemsContainer) return;
const scrollTop = viewport.scrollTop;
const startIdx = Math.floor(scrollTop / this._itemHeight);
const endIdx = Math.min(startIdx + this._visibleCount + 2, this._items.length);
itemsContainer.style.top = `px`;
itemsContainer.innerHTML = this._items
.slice(startIdx, endIdx)
.map((item, i) => ``)
.join('');
}
}
customElements.define('virtual-list', VirtualList);
// 3. Preload Critical Components
//
//
// 4. Service Worker Caching
// if ('serviceWorker' in navigator) {
// navigator.serviceWorker.register('/sw.js');
// }
//
// // sw.js
// const CACHE_NAME = 'components-v1';
// const COMPONENT_URLS = [
// '/components/product-card.js',
// '/components/nav-bar.js',
// '/components/data-table.js',
// ];
//
// self.addEventListener('install', (event) => {
// event.waitUntil(
// caches.open(CACHE_NAME).then(cache => cache.addAll(COMPONENT_URLS))
// );
// });
console.log("Performance optimizations defined");
Production Deployment
Deploy Web Components ที่ edge
# === Production Deployment ===
# 1. Build Configuration
# ===================================
cat > package.json << 'EOF'
{
"name": "edge-web-components",
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "esbuild src/components/*.js --bundle --minify --outdir=dist/components",
"build:ssr": "esbuild src/ssr/*.js --bundle --platform=node --outdir=dist/ssr",
"dev": "esbuild src/components/*.js --bundle --outdir=dist/components --watch --servedir=public",
"test": "web-test-runner 'test/**/*.test.js' --node-resolve",
"deploy": "wrangler deploy"
},
"devDependencies": {
"esbuild": "^0.20.0",
"@web/test-runner": "^0.18.0",
"wrangler": "^3.0.0"
}
}
EOF
# 2. Cloudflare Workers Deployment
# ===================================
cat > wrangler.toml << 'EOF'
name = "web-components-edge"
main = "src/worker.js"
compatibility_date = "2025-01-15"
[site]
bucket = "./dist"
[[routes]]
pattern = "shop.example.com/*"
zone_name = "example.com"
EOF
# 3. Testing Web Components
# ===================================
cat > test/product-card.test.js << 'EOF'
import { expect, fixture, html } from '@open-wc/testing';
import '../src/components/product-card.js';
describe('ProductCard', () => {
it('renders with default values', async () => {
const el = await fixture(html` `);
expect(el.shadowRoot).to.exist;
});
it('displays product name', async () => {
const el = await fixture(
html` `
);
const name = el.shadowRoot.querySelector('.name');
expect(name.textContent).to.equal('Test Product');
});
it('fires add-to-cart event', async () => {
const el = await fixture(
html` `
);
let eventFired = false;
el.addEventListener('add-to-cart', (e) => {
eventFired = true;
expect(e.detail.name).to.equal('Test');
});
el.shadowRoot.querySelector('button').click();
expect(eventFired).to.be.true;
});
});
EOF
# 4. Deploy Steps
# ===================================
npm install
npm run build
npm run test
# Deploy to Cloudflare Workers
# wrangler deploy
# Deploy static assets to CDN
# wrangler pages deploy dist/
# 5. Monitoring
# ===================================
# Key metrics:
# - Component render time (SSR)
# - Time to Interactive (TTI)
# - Largest Contentful Paint (LCP)
# - Cache hit rate at edge
# - Component bundle sizes
echo "Deployment configured"
FAQ คำถามที่พบบ่อย
Q: Web Components กับ React Components ต่างกันอย่างไร?
A: Web Components เป็น native browser standard ไม่ต้องใช้ framework ทำงานได้ทุกที่ที่มี browser มี Shadow DOM สำหรับ style encapsulation ขนาดเล็กกว่า (ไม่มี runtime overhead) React Components ต้องใช้ React library (42KB gzipped) มี virtual DOM, JSX, hooks ที่ developer experience ดีกว่า ecosystem ใหญ่กว่ามาก ใช้ Web Components สำหรับ design systems ที่ใช้ข้าม frameworks, micro-frontends, simple interactive widgets ใช้ React สำหรับ complex SPAs ที่ต้องการ rich ecosystem
Q: Declarative Shadow DOM (DSD) คืออะไร?
A: DSD เป็น feature ใหม่ของ HTML ที่ให้สร้าง Shadow DOM ใน HTML markup โดยไม่ต้อง JavaScript ทำให้ SSR Web Components ได้จริง ก่อนมี DSD ต้อง JavaScript run ก่อนถึงจะเห็น Shadow DOM content ทำให้ SSR ไม่มีประโยชน์ ตอนนี้ Chrome, Edge, Safari รองรับแล้ว Firefox กำลังพัฒนา ใช้ template shadowrootmode="open" เป็น syntax สำคัญสำหรับ edge SSR ของ Web Components
Q: Lit กับ vanilla Web Components ต่างกันไหม?
A: Lit เป็น tiny library (5KB) จาก Google ที่ช่วยเขียน Web Components ง่ายขึ้น มี reactive properties, efficient rendering ด้วย tagged template literals, decorators สำหรับ TypeScript vanilla Web Components ไม่ต้องใช้ library เพิ่ม แต่ต้องเขียน boilerplate มากกว่า render ด้วย innerHTML ซึ่ง performance อาจไม่ดีเท่า Lit แนะนำ Lit สำหรับ production projects vanilla สำหรับ learning หรือ simple components
Q: Web Components เหมาะกับ edge computing อย่างไร?
A: เหมาะมากเพราะ Web Components เป็น standard HTML ไม่ต้อง hydration ซับซ้อนเหมือน React SSR, Declarative Shadow DOM ให้ SSR ที่ edge ได้โดยตรง, component-level caching cache แต่ละ component แยกกัน reuse ได้, streaming ส่ง components ทีละตัวไม่ต้องรอ render ทั้ง page, ขนาดเล็กไม่มี framework JS ที่ต้อง download ทำให้ Time to Interactive เร็ว เหมาะสำหรับ e-commerce, content sites, marketing pages
