Manipulación del DOM Avanzada
Técnicas sofisticadas para interactuar con el DOM.
Cristian Escalante
Última actualización: 23 de abril de 2025
Manipulación del DOM Avanzada
Técnicas sofisticadas para interactuar con el DOM:
- Manipulación eficiente del DOM
- Traversing avanzado
- Virtual DOM y su concepto
- Web Components
- Shadow DOM
- Templates y slots
- IntersectionObserver y LazyLoading
- Animaciones con JavaScript
Manipulación eficiente del DOM
La manipulación constante del DOM puede afectar negativamente al rendimiento de una aplicación web. Aquí hay algunas técnicas para optimizar la manipulación del DOM:
Minimizar la recomputación del layout
// Ineficiente: causa múltiples reflows
for (let i = 0; i < 100; i++) {
elemento.style.width = i + 'px';
}
// Eficiente: agrupa cambios
function actualizarEstilos() {
// Leer
const width = elemento.offsetWidth;
// Agrupar escrituras
elemento.style.width = (width + 10) + 'px';
elemento.style.height = (width + 10) + 'px';
elemento.style.margin = (width / 10) + 'px';
}
Uso de fragmentos de documento
// Crear un fragmento (no forma parte del DOM)
const fragmento = document.createDocumentFragment();
// Añadir elementos al fragmento
for (let i = 0; i < 1000; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
fragmento.appendChild(item);
}
// Insertar el fragmento en el DOM (una sola operación)
document.getElementById('lista').appendChild(fragmento);
Modificación por lotes
// Ocultar elemento durante modificaciones
const elemento = document.getElementById('contenedor');
const displayOriginal = elemento.style.display;
elemento.style.display = 'none';
// Realizar múltiples modificaciones
realizarCambiosComplejos(elemento);
// Mostrar elemento nuevamente
elemento.style.display = displayOriginal;
Traversing avanzado del DOM
El traversing del DOM se refiere a la navegación a través de los nodos del árbol del documento.
Relaciones entre nodos
const elemento = document.querySelector('.item');
// Navegación vertical
const padre = elemento.parentNode; // o parentElement
const hijos = elemento.children; // solo elementos (no texto ni comentarios)
const todosLosHijos = elemento.childNodes; // todos los tipos de nodos
const primerHijo = elemento.firstChild; // primer nodo hijo
const primerElementoHijo = elemento.firstElementChild; // primer elemento hijo
const ultimoHijo = elemento.lastChild; // último nodo hijo
const ultimoElementoHijo = elemento.lastElementChild; // último elemento hijo
// Navegación horizontal
const siguienteHermano = elemento.nextSibling; // siguiente nodo hermano
const siguienteElementoHermano = elemento.nextElementSibling; // siguiente elemento hermano
const anteriorHermano = elemento.previousSibling; // anterior nodo hermano
const anteriorElementoHermano = elemento.previousElementSibling; // anterior elemento hermano
Búsqueda avanzada de elementos
// Buscar el ancestro más cercano que coincida con un selector
const ancestro = elemento.closest('.contenedor');
// Comprobar si un elemento contiene a otro
const contiene = padre.contains(hijo);
// Obtener la posición de un elemento entre sus hermanos
const indice = Array.from(elemento.parentNode.children).indexOf(elemento);
Virtual DOM y su concepto
El Virtual DOM es una representación ligera en memoria del DOM real utilizada por frameworks como React para optimizar las actualizaciones de la interfaz de usuario.
Cómo funciona el Virtual DOM
- Creación: Se crea una representación virtual del DOM en memoria.
- Manipulación: Las operaciones se realizan sobre esta copia virtual.
- Reconciliación: Se compara el estado anterior con el nuevo.
- Actualización: Solo se aplican al DOM real los cambios necesarios.
// Implementación simplificada de un Virtual DOM
class VirtualNode {
constructor(tagName, props = {}, children = []) {
this.tagName = tagName;
this.props = props;
this.children = children;
}
render() {
// Crear el elemento DOM real
const element = document.createElement(this.tagName);
// Aplicar propiedades
Object.entries(this.props).forEach(([key, value]) => {
if (key === 'style' && typeof value === 'object') {
Object.assign(element.style, value);
} else if (key.startsWith('on') && typeof value === 'function') {
element.addEventListener(key.substring(2).toLowerCase(), value);
} else {
element.setAttribute(key, value);
}
});
// Renderizar hijos
this.children.forEach(child => {
if (typeof child === 'string') {
element.appendChild(document.createTextNode(child));
} else {
element.appendChild(child.render());
}
});
return element;
}
}
// Ejemplo de uso
const vNode = new VirtualNode('div', { class: 'container' }, [
new VirtualNode('h1', {}, ['Título']),
new VirtualNode('p', { style: { color: 'blue' } }, ['Contenido'])
]);
// Renderizar en el DOM
document.body.appendChild(vNode.render());
Web Components
Los Web Components son un conjunto de tecnologías que permiten crear elementos HTML personalizados reutilizables.
Componentes principales
- Custom Elements: API para definir elementos HTML personalizados
- Shadow DOM: Encapsulación de estilos y estructura
- HTML Templates: Fragmentos HTML declarativos
- HTML Imports: Incluir y reutilizar HTML (obsoleto)
Creación de un Custom Element
// Definir la clase del componente
class MiComponente extends HTMLElement {
constructor() {
super();
// Crear Shadow DOM
this.attachShadow({ mode: 'open' });
// Definir estructura interna
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
border: 1px solid #ccc;
padding: 16px;
}
.titulo {
color: blue;
}
</style>
<div>
<h2 class="titulo">${this.getAttribute('titulo') || 'Título predeterminado'}</h2>
<slot></slot>
</div>
`;
}
// Ciclo de vida: cuando el elemento se añade al DOM
connectedCallback() {
console.log('Componente añadido al DOM');
}
// Ciclo de vida: cuando el elemento se elimina del DOM
disconnectedCallback() {
console.log('Componente eliminado del DOM');
}
// Ciclo de vida: cuando cambia un atributo observado
attributeChangedCallback(nombre, valorAntiguo, valorNuevo) {
if (nombre === 'titulo' && this.shadowRoot) {
this.shadowRoot.querySelector('.titulo').textContent = valorNuevo;
}
}
// Definir qué atributos observar
static get observedAttributes() {
return ['titulo'];
}
}
// Registrar el componente
customElements.define('mi-componente', MiComponente);
Uso del componente personalizado
<mi-componente titulo="Mi título personalizado">
<p>Este contenido se insertará en el slot</p>
</mi-componente>
Shadow DOM
El Shadow DOM permite encapsular el marcado, el estilo y el comportamiento de un componente, aislándolo del resto del documento.
Creación y uso del Shadow DOM
// Crear un elemento host
const host = document.createElement('div');
document.body.appendChild(host);
// Crear y adjuntar un Shadow DOM
const shadowRoot = host.attachShadow({ mode: 'open' });
// mode: 'open' permite acceder desde JavaScript externo
// mode: 'closed' oculta el shadowRoot de JavaScript externo
// Añadir contenido al Shadow DOM
shadowRoot.innerHTML = `
<style>
/* Estos estilos solo afectan al interior del Shadow DOM */
p {
color: red;
font-weight: bold;
}
</style>
<p>Este texto está dentro del Shadow DOM</p>
`;
// Los estilos del documento principal no afectan al contenido del Shadow DOM
document.head.innerHTML += `
<style>
p {
color: blue;
font-style: italic;
}
</style>
`;
Encapsulación de estilos
// Selectores especiales en Shadow DOM
shadowRoot.innerHTML += `
<style>
/* Estilo para el elemento host */
:host {
display: block;
border: 1px solid black;
}
/* Estilo para el host cuando tiene una clase */
:host(.destacado) {
background-color: yellow;
}
/* Estilo para el host basado en el contexto */
:host-context(.tema-oscuro) {
background-color: #333;
color: white;
}
</style>
`;
Templates y slots
Las plantillas HTML y los slots permiten definir fragmentos de HTML reutilizables con espacios reservados para contenido personalizado.
Uso de templates
<!-- Definición de una plantilla -->
<template id="mi-template">
<style>
.contenedor {
border: 1px solid #ccc;
padding: 10px;
}
.titulo {
color: navy;
}
</style>
<div class="contenedor">
<h2 class="titulo">Título de la plantilla</h2>
<slot name="contenido">Contenido predeterminado</slot>
<footer>
<slot name="pie">Pie predeterminado</slot>
</footer>
</div>
</template>
// Uso de la plantilla en JavaScript
class ComponenteConTemplate extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// Clonar el contenido de la plantilla
const template = document.getElementById('mi-template');
const contenido = template.content.cloneNode(true);
// Añadir al Shadow DOM
this.shadowRoot.appendChild(contenido);
}
}
customElements.define('componente-template', ComponenteConTemplate);
Uso de slots
<!-- Uso del componente con slots -->
<componente-template>
<div slot="contenido">
<p>Este es mi contenido personalizado</p>
</div>
<p slot="pie">Copyright 2023</p>
</componente-template>
IntersectionObserver y LazyLoading
El IntersectionObserver API permite detectar cuándo un elemento entra o sale del viewport del navegador, lo que facilita implementar técnicas como lazy loading.
Implementación básica de IntersectionObserver
// Crear un observador
const observador = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
// entry.isIntersecting será true cuando el elemento sea visible
if (entry.isIntersecting) {
console.log('El elemento es visible en el viewport');
// Opcional: dejar de observar el elemento
observer.unobserve(entry.target);
}
});
}, {
// Opciones
root: null, // viewport
rootMargin: '0px', // margen alrededor del root
threshold: 0.1 // porcentaje de visibilidad necesario (10%)
});
// Observar un elemento
const elemento = document.querySelector('.mi-elemento');
observador.observe(elemento);
Implementación de lazy loading de imágenes
// Función para cargar imágenes cuando sean visibles
function lazyLoadImages() {
const imagenes = document.querySelectorAll('img[data-src]');
const observador = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
// Cargar la imagen real
img.src = img.dataset.src;
// Eliminar el atributo data-src
img.removeAttribute('data-src');
// Dejar de observar la imagen
observer.unobserve(img);
}
});
});
// Observar todas las imágenes
imagenes.forEach(img => observador.observe(img));
}
// Iniciar lazy loading
document.addEventListener('DOMContentLoaded', lazyLoadImages);
HTML con imágenes para lazy loading
<img src="placeholder.jpg" data-src="imagen-real-1.jpg" alt="Descripción">
<img src="placeholder.jpg" data-src="imagen-real-2.jpg" alt="Descripción">
<img src="placeholder.jpg" data-src="imagen-real-3.jpg" alt="Descripción">
Animaciones con JavaScript
JavaScript permite crear animaciones manipulando propiedades de elementos a lo largo del tiempo.
Animación con requestAnimationFrame
function animar(elemento, propiedades, duracion) {
const inicio = performance.now();
const valoresIniciales = {};
const cambios = {};
// Calcular valores iniciales y cambios
for (const prop in propiedades) {
valoresIniciales[prop] = parseFloat(getComputedStyle(elemento)[prop]) || 0;
cambios[prop] = propiedades[prop] - valoresIniciales[prop];
}
// Función de animación
function paso(tiempoActual) {
const tiempoTranscurrido = tiempoActual - inicio;
const fraccion = Math.min(tiempoTranscurrido / duracion, 1);
// Actualizar propiedades
for (const prop in propiedades) {
const valor = valoresIniciales[prop] + cambios[prop] * fraccion;
elemento.style[prop] = `${valor}px`;
}
// Continuar la animación si no ha terminado
if (fraccion < 1) {
requestAnimationFrame(paso);
}
}
// Iniciar animación
requestAnimationFrame(paso);
}
// Ejemplo de uso
const caja = document.querySelector('.caja');
animar(caja, { width: 300, height: 200 }, 1000);
Funciones de easing
const funciones = {
// Lineal
lineal: t => t,
// Ease in
easeInQuad: t => t * t,
easeInCubic: t => t * t * t,
// Ease out
easeOutQuad: t => t * (2 - t),
easeOutCubic: t => (--t) * t * t + 1,
// Ease in-out
easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
};
// Animación con función de easing
function animarConEasing(elemento, propiedades, duracion, funcionEasing) {
const inicio = performance.now();
const valoresIniciales = {};
const cambios = {};
for (const prop in propiedades) {
valoresIniciales[prop] = parseFloat(getComputedStyle(elemento)[prop]) || 0;
cambios[prop] = propiedades[prop] - valoresIniciales[prop];
}
function paso(tiempoActual) {
const tiempoTranscurrido = tiempoActual - inicio;
const fraccion = Math.min(tiempoTranscurrido / duracion, 1);
const fraccionConEasing = funcionEasing(fraccion);
for (const prop in propiedades) {
const valor = valoresIniciales[prop] + cambios[prop] * fraccionConEasing;
elemento.style[prop] = `${valor}px`;
}
if (fraccion < 1) {
requestAnimationFrame(paso);
}
}
requestAnimationFrame(paso);
}
// Ejemplo
animarConEasing(caja, { width: 300 }, 1000, funciones.easeOutCubic);
Web Animations API
// Animación usando la API nativa
elemento.animate([
// Fotogramas clave (keyframes)
{ transform: 'translateX(0)', opacity: 1 },
{ transform: 'translateX(100px)', opacity: 0.5, offset: 0.7 },
{ transform: 'translateX(200px)', opacity: 0 }
], {
// Opciones
duration: 2000, // duración en ms
easing: 'ease-in-out', // función de temporización
delay: 500, // retraso antes de iniciar
iterations: 2, // número de repeticiones
direction: 'alternate', // alternar dirección en repeticiones
fill: 'forwards' // mantener el estado final
});
// Control de la animación
const animacion = elemento.animate(/* ... */);
animacion.pause(); // pausar
animacion.play(); // reproducir
animacion.reverse(); // invertir dirección
animacion.finish(); // ir al final
animacion.cancel(); // cancelar
// Eventos
animacion.onfinish = () => console.log('Animación finalizada');
animacion.oncancel = () => console.log('Animación cancelada');