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 only what's needed
import debounce from 'lodash/debounce'
import throttle from 'lodash/throttle'
Lazy Load Heavy Dependencies
Load large libraries only when needed:
export class MyChart extends HTMLElement {
private chartLib: any = null
async loadChartLibrary() {
if (!this.chartLib) {
// Only 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 only 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 only when necessary.
- Combine related components into one 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
const API_KEY = 'sk_live_1234567890'
// ✅ Good - use environment variables
const API_KEY = process.env.BYO_API_KEY
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.
- Validate that BYO is necessary.
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 onclick="handleClick()">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:
- BrowserStack.
- Sauce Labs.
- 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). - Bundle size is acceptable (< 100KB gzipped).
- Tested in dev harness (
npx byo preview). - Documented usage and properties.
- Version updated in
package.json. - 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 - only modifies own DOM
this.querySelector('.my-component').classList.add('active')
Don't Block the Main Thread
Avoid synchronous 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() {
// Listen for custom command events
this.addEventListener('increment', this.handleIncrement)
}
disconnectedCallback() {
// Always clean up event listeners
this.removeEventListener('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('increment', handleIncrement)
// Cleanup function automatically called on unmount
return () => {
host.removeEventListener('increment', handleIncrement)
}
}, [])
Vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const count = ref(0)
let hostElement = null
const handleIncrement = () => {
count.value++
}
onMounted(() => {
const instance = getCurrentInstance()
hostElement = instance?.proxy?.$el?.getRootNode()?.host
if (!hostElement) return
hostElement.addEventListener('increment', handleIncrement)
})
// Always pair onMounted with onUnmounted
onUnmounted(() => {
if (!hostElement) return
hostElement.removeEventListener('increment', handleIncrement)
})
</script>
Angular
export class MyComponent implements OnDestroy {
private hostElement: HTMLElement | null = null
ngAfterViewInit() {
this.hostElement = this.elementRef.nativeElement.getRootNode()?.host
if (!this.hostElement) return
this.hostElement.addEventListener('increment', this.handleIncrement)
}
ngOnDestroy() {
if (this.hostElement) {
this.hostElement.removeEventListener('increment', this.handleIncrement)
}
}
private handleIncrement = () => {
this.count++
}
}
Lit
export class MyComponent extends LitElement {
private handleIncrement = () => {
this._count++
}
override connectedCallback() {
super.connectedCallback()
this.addEventListener('increment', this.handleIncrement)
}
override disconnectedCallback() {
super.disconnectedCallback()
this.removeEventListener('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
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-05-01 | Initial publication |