Follow the best practices below to build secure, high-performance, and maintainable BYO components.
Performance
Bundle Size Optimization
Minimize Bundle Size
Every BYO asset adds to the page load, so keep bundles small:
// ❌ Bad - imports entire library
import * as _ from 'lodash'
// ✅ Good - imports what's needed
import debounce from 'lodash/debounce'
import throttle from 'lodash/throttle'
Lazy Load Heavy Dependencies
Load large libraries when needed:
export class MyChart extends HTMLElement {
private chartLib: any = null
async loadChartLibrary() {
if (!this.chartLib) {
// load when actually rendering a chart
this.chartLib = await import('d3')
}
return this.chartLib
}
async renderChart() {
const d3 = await this.loadChartLibrary()
// use d3...
}
}
Use Code Splitting
Split components into separate bundles:
// src/entry.ts
export const myButton = {
view: async () => (await import('./MyButton')).MyButton,
model: async () => (await import('./MyButton.model')).MyButtonModel,
}
export const myChart = {
view: async () => (await import('./MyChart')).MyChart,
model: async () => (await import('./MyChart.model')).MyChartModel,
}
The page loads the components it uses.
Monitor Bundle Size
Check the production bundle size:
npm run build
ls -lh dist/*.js
# Target: < 100KB per component (minified + gzipped)
Limit the Number of BYO Assets
Every BYO asset adds overhead to module loading. Follow the guidelines below:
Use BYO when necessary.
Combine related components into asset.
Avoid creating many single-purpose components.
Reuse components across applications.
Example:
❌ Bad - separate assets
- text-input.tar.gz
- number-input.tar.gz
- email-input.tar.gz
✅ Good - combined asset
- form-inputs.tar.gz (contains all three)
Limit How Often Operations Run
Rate-limit submission updates (debouncing):
import debounce from 'lodash/debounce'
export class MyInput extends HTMLElement {
private updateSubmission = debounce((value: string) => {
this.onSubmissionUpdate?.(value)
}, 300)
handleInput(e: Event) {
const value = (e.target as HTMLInputElement).value
this.updateSubmission(value)
}
}
Limit how often events fire (throttling):
import throttle from 'lodash/throttle'
export class MySlider extends HTMLElement {
private handleSlide = throttle((value: number) => {
this.onValueChanged?.(value)
}, 100)
}
Batch DOM Reads and Writes
Batch DOM reads before writes to avoid forcing the browser to recalculate layout on every change:
// ❌ Bad - interleaved reads and writes (causes reflow)
element.style.width = '100px'
const height = element.offsetHeight // forces reflow
element.style.height = `${height}px`
// ✅ Good - batch reads, then writes
const height = element.offsetHeight
requestAnimationFrame(() => {
element.style.width = '100px'
element.style.height = `${height}px`
})
Security
Prevent Cross-Site Scripting (XSS) Attacks
Never use innerHTML with user input:
// ❌ Bad - vulnerable to XSS
set label(value: string) {
this.querySelector('.label')!.innerHTML = value
}
// ✅ Good - escapes HTML
set label(value: string) {
this.querySelector('.label')!.textContent = value
}
Sanitize HTML if you must render it:
import DOMPurify from 'dompurify'
set richText(value: string) {
const clean = DOMPurify.sanitize(value)
this.querySelector('.content')!.innerHTML = clean
}
Validate Input
Validate all prop values:
set count(value: unknown) {
// Validate type
if (typeof value !== 'number') {
console.warn('Invalid count value:', value)
return
}
// Validate range
if (value < 0 || value > 1000) {
console.warn('Count out of range:', value)
return
}
this._count = value
this.render()
}
Secure Third-Party Libraries
Audit dependencies:
# Check for known vulnerabilities
npm audit
# Fix vulnerabilities
npm audit fix
Keep libraries updated:
# Check for outdated packages
npm outdated
# Update dependencies
npm update
Review library code:
Use well-maintained libraries with active communities.
Check GitHub issues for security concerns.
Review license compatibility.
Avoid deprecated libraries.
Don't Expose Sensitive Data
Never log sensitive data:
// ❌ Bad
console.log('User data:', submissionData)
// ✅ Good
console.log('Submission updated')
Don't store secrets in code:
// ❌ Bad - hardcoded secret in bundle
const API_KEY = 'sk_live_1234567890'
Never store credentials, API keys, or tokens directly in component code. The compiled bundle is downloadable by anyone with access to the page.
Maintainability
Validate Use Cases First
Before creating a BYO component, complete the following steps:
Check if built-in components meet your needs.
Explore logic components for business logic.
Consider if CSS styling is sufficient.
Validating first avoids duplicating platform functionality and reduces maintenance overhead.
Document Your Components
Add JSDoc comments:
/**
* Custom task list component.
*
* Displays a list of tasks with checkboxes and delete buttons.
*
* @example
* ```typescript
* element.items = [
* { id: '1', title: 'Task 1', completed: false }
* ]
* ```
*/
export class TaskList extends HTMLElement {
/**
* Array of task items to display.
*/
set items(value: Task[]) {
this._items = value
this.render()
}
}
Create a README:
# Task List Component
Custom component for displaying and managing tasks.
## Props
- `label` (string) - List heading
- `items` (Task[]) - Array of tasks
- `allowAdd` (boolean) - Show add button
- `allowDelete` (boolean) - Show delete buttons
## Events
- `onTaskToggled` - Fires when task is checked/unchecked
- `onTaskAdded` - Fires when add button is clicked
- `onTaskDeleted` - Fires when delete button is clicked
## Usage
See examples in [Module Builder Documentation](#).
Version Your Components
Use semantic versioning:
{
"version": "1.2.3"
}
Major (1.x.x): Breaking changes.
Minor (x.2.x): New backward-compatible features.
Patch (x.x.3): Bug fixes.
Document breaking changes:
# CHANGELOG.md
## [2.0.0] - 2026-05-01
### Breaking Changes
- Renamed `onItemClick` to `onItemSelected`
- Changed `items` prop structure (now requires `id` field)
### Migration Guide
Update event handlers from `onItemClick` to `onItemSelected`.
Add `id` field to all items: `{ id: '1', ...rest }`.
Centralize Updates
One asset, many uses:
When you update a BYO asset, changes reflect everywhere it's used. Plan updates carefully:
Test changes thoroughly in staging.
Communicate updates to Creators.
Provide migration guides for breaking changes.
Consider backward compatibility.
Plan for Library Updates
Track third-party library versions:
{
"dependencies": {
"d3": "7.8.5",
"lodash": "4.17.21"
}
}
Set up dependency monitoring:
Enable Dependabot or Renovate.
Subscribe to library release notes.
Test updates in staging before production.
Accessibility
Semantic HTML
Use proper HTML elements:
// ❌ Bad - div button
this.innerHTML = `
<div>Click me</div>
`
// ✅ Good - semantic button
this.innerHTML = `
<button>Click me</button>
`
ARIA Labels
Add labels for screen readers:
this.innerHTML = `
<button aria-label="Delete task: ${task.title}">
<span aria-hidden="true">×</span>
</button>
`
Keyboard Navigation
Support keyboard interactions:
connectedCallback() {
this.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
this.handleClick()
}
})
}
Make elements focusable:
this.innerHTML = `
<div class="card" tabindex="0" role="button">
...
</div>
`
Color Contrast
Ensure sufficient contrast:
/* ❌ Bad - low contrast (2.5:1) */
.label {
color: #999;
background: #fff;
}
/* ✅ Good - high contrast (7:1) */
.label {
color: #333;
background: #fff;
}
Test with tools:
Focus Management
Visible focus indicators:
button:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
Don't remove focus styles:
/* ❌ Bad */
*:focus {
outline: none;
}
/* ✅ Good - custom but visible */
*:focus-visible {
outline: 2px solid currentColor;
}
Error Handling
Graceful Degradation
Handle missing callbacks:
handleClick() {
// Always use optional chaining
this.onButtonClicked?.({ label: this._label })
}
Validate properties:
set items(value: unknown) {
if (!Array.isArray(value)) {
console.warn('items must be an array')
this._items = []
return
}
this._items = value
this.render()
}
Error Boundaries
Wrap error-prone code:
async loadData() {
try {
const data = await import('./heavy-library.js')
return data
} catch (error) {
console.error('Failed to load library:', error)
this.showError('Failed to load component')
return null
}
}
User-Friendly Messages
Show helpful errors:
// ❌ Bad
throw new Error('Invalid input')
// ✅ Good
console.error('Invalid items prop: expected array, got', typeof value)
this.showError('Unable to display items. Please contact support.')
Testing
Test Across Browsers
Minimum browser support:
Chrome, Firefox, Safari, and Edge: two most recent versions of each.
Use cross-browser testing:
Local testing with different browsers.
Test Edge Cases
Empty states:
it('handles empty items array', () => {
element.items = []
expect(element.querySelector('.task')).toBeNull()
})
Invalid input:
it('handles invalid items gracefully', () => {
element.items = null as any
expect(() => element.items = null).not.toThrow()
})
Large datasets:
it('handles 1000 items', () => {
const items = Array.from({ length: 1000 }, (_, i) => ({
id: String(i),
title: `Task ${i}`,
completed: false,
}))
element.items = items
expect(element.querySelectorAll('.task')).toHaveLength(1000)
})
Automated Testing
Set up automated testing:
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
- run: npm run lint
Deployment
Pre-Deployment Checklist
Before uploading to Unqork, verify the following:
All tests pass (
npm test).Linting passes (
npm run lint).Build succeeds (
npm run build:prod).Bundle size is acceptable (< 100KB gzipped).
Tested in dev harness (
npm run preview).Documented usage and properties.
Version updated —
npm run build:prodbumps the patch version automatically; bump major or minor manually inpackage.jsonfor breaking or feature changes.CHANGELOG updated.
Staging First
Deploy to staging before production:
Upload to the staging environment.
Test in real modules.
Verify integration with other components.
Get stakeholder approval.
Deploy to production.
Monitor After Deployment
Check for issues:
Browser console errors.
Performance regressions.
User feedback.
Error tracking tools like Sentry.
Common Pitfalls
Avoid DOM Manipulation Outside the Component
Don't modify the outer DOM:
// ❌ Bad - manipulates platform DOM
document.querySelector('.unqork-module').classList.add('custom')
// ✅ Good - modifies own DOM
this.querySelector('.my-component').classList.add('active')
Don't Block the Main Thread
Avoid heavy operations:
// ❌ Bad - blocks UI
function processLargeDataset(items: Item[]) {
return items.map(complexCalculation)
}
// ✅ Good - async with batching
async function processLargeDataset(items: Item[]) {
const batchSize = 100
const results = []
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize)
results.push(...batch.map(complexCalculation))
// Yield to browser
await new Promise(resolve => setTimeout(resolve, 0))
}
return results
}
Clean Up Resources
Always remove event listeners in disconnectedCallback():
Without cleanup, event listeners keep references to the component instance, causing memory leaks as the browser cannot reclaim unused memory.
private handleIncrement = () => {
this._count++
this.render()
}
connectedCallback() {
// Commands are namespaced as <componentType>:<commandType>
this.addEventListener('myComponent:increment', this.handleIncrement)
}
disconnectedCallback() {
// Always clean up event listeners
this.removeEventListener('myComponent:increment', this.handleIncrement)
}
Framework-specific cleanup:
React
useEffect(() => {
const host = containerRef.current?.getRootNode()?.host
if (!host) return
const handleIncrement = () => {
setCount(c => c + 1)
}
host.addEventListener('myComponent:increment', handleIncrement)
// Cleanup function automatically called on unmount
return () => {
host.removeEventListener('myComponent:increment', handleIncrement)
}
}, [])
Vue
<script setup>
import { ref,, } from 'vue'
const count = ref(0)
let hostElement = null
const handleIncrement = () => {
count.value++
}(() => {
const instance = getCurrentInstance()
hostElement = instance?.proxy?.$el?.getRootNode()?.host
if (!hostElement) return
hostElement.addEventListener('myComponent:increment', handleIncrement)
})
// Always pair with(() => {
if (!hostElement) return
hostElement.removeEventListener('myComponent:increment', handleIncrement)
})
</script>
Angular
export class MyComponent implements {
private hostElement: HTMLElement | null = null
ngAfterViewInit() {
this.hostElement = this.elementRef.nativeElement.getRootNode()?.host
if (!this.hostElement) return
this.hostElement.addEventListener('myComponent:increment', this.handleIncrement)
}
ngOnDestroy() {
if (this.hostElement) {
this.hostElement.removeEventListener('myComponent:increment', this.handleIncrement)
}
}
private handleIncrement = () => {
this.count++
}
}
Lit
export class MyComponent extends LitElement {
private handleIncrement = () => {
this._count++
}
override connectedCallback() {
super.connectedCallback()
this.addEventListener('myComponent:increment', this.handleIncrement)
}
override disconnectedCallback() {
super.disconnectedCallback()
this.removeEventListener('myComponent:increment', this.handleIncrement)
}
}
Cancel async operations:
private abortController: AbortController | null = null
async fetchData() {
this.abortController = new AbortController()
try {
const response = await fetch(url, {
signal: this.abortController.signal
})
return response.json()
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch cancelled')
}
}
}
disconnectedCallback() {
this.abortController?.abort()
}
Summary
Key Takeaways:
Keep bundles small by splitting your bundle and loading code on demand.
Validate use cases before creating BYO components.
Prevent cross-site scripting (XSS) attacks and validate all inputs.
Make components accessible through ARIA labels, keyboard navigation, and color contrast.
Test thoroughly across browsers and edge cases.
Document usage and maintain version history.
Monitor performance and errors after deployment.
Next Steps:
Review Styling Guide for CSS best practices.
Review When to Use BYO guidelines.
Check FAQ for common questions.
See Troubleshooting for issues.
Changelog
Date | Change |
|---|---|
2026-09-30 | Replaced |
2026-05-01 | Initial publication |