De tres renderers a uno: cómo extraer un núcleo de render compartido
Hay una deuda técnica que no aparece como bug: el mismo bloque de código funciona en varios sitios, pero empieza a evolucionar distinto en cada uno. En mi caso era el renderer de Markdown enriquecido de mi plataforma.
ImportantLa decisión no fue "refactorizar porque hay duplicación". Fue extraer una pieza que ya tenía contrato propio: Markdown enriquecido entra, HTML seguro y decorado sale.
Vista rápida
| Pregunta | Respuesta corta |
|---|---|
| ¿Qué se repitió? | El renderer de Markdown enriquecido |
| ¿Dónde vivía? | El editor desktop, un dashboard interno y un visor web |
| ¿Por qué dolía? | Cada copia aceptaba features distintas y bugs distintos |
| ¿Qué extraje? | Un paquete compartido con el contrato de render |
| ¿Qué no moví? | UI del editor, navegación, i18n y acciones de producto |
El estado de partida
Cuando hice la auditoría, encontré tres copias funcionales del mismo renderer en distintos consumidores: el editor canónico, una extracción parcial pensada para vistas internas y un reader vendorizado pensado para web.
| Copia | Rol real | Fortalezas | Riesgo |
|---|---|---|---|
| Renderer del editor | Preview canónico | Footnotes, diagramas avanzados, decoradores de layout | Mezclaba core con detalles del editor |
| Renderer del dashboard | Extracción parcial | Más modular y legible | Le faltaban features del editor |
| Reader web vendorizado | Visor público | Funcionaba sin build complejo | Iba por detrás y dependía de scripts de sync |
Las tres usaban el mismo stack base: parser, sanitizador, resaltado de código, fórmulas y diagramas. El problema no era que hubiese tres archivos parecidos. El problema era que el lector veía tres versiones de lo que debería ser el mismo documento.
WarningEl olor a deuda no es repetición. Es repetición que deriva.
La señal visual
La señal más clara fue hacer una lista de bloques soportados. Si una tabla, una alerta o un diagrama renderiza distinto según el producto, el contrato ya está roto.
| Bloque Markdown | Editor | Dashboard | Visor web | Resultado deseado |
|---|---|---|---|---|
| Diagramas | Completo | Básico | Vendorizado | Un solo render |
| Fórmulas | Sí | Sí | Sí | Un solo render |
| Alertas tipo callout | Sí | Parcial | Sí | Un solo decorador |
| Columnas | Sí | Sí | Sí | Un solo decorador |
| Footnotes | Sí | No | Depende | Feature del core |
| Estilos de tabla | Sí | Parcial | Parcial | Feature del core |
La tabla cambió la conversación: ya no era un refactor abstracto, era una promesa de lectura que debía ser consistente.
Criterio para extraer
Uso tres preguntas antes de sacar código a un paquete:
Identidad
¿La pieza tiene entrada, salida y contrato propios?
En este caso sí: Markdown + dependencias de runtime → HTML seguro + decoradores.
Semántica
¿Los consumidores quieren el mismo comportamiento?
Sí. Nadie quería "otra forma" de renderizar tablas o diagramas.
Próximo consumidor
¿Hay más usos reales en cola?
Sí: varios consumidores adicionales, incluyendo exports server-side y portales.
Si una de esas respuestas hubiese sido no, habría esperado. Como las tres eran sí, la extracción ya no era estética: era mantenimiento preventivo.
Qué cambió al extraer
La extracción no consistió en diseñar un renderer nuevo. Consistió en mover el contrato existente al lugar correcto y limpiar las dependencias.
1. Dependencias inyectadas
Antes, las copias leían sus librerías de runtime desde el ámbito global del navegador. Eso encaja en el cliente, pero no en un export server-side.
const preview = new PreviewRenderer({
element,
dependencies: {
parser,
sanitizer,
highlighter,
diagramEngine,
mathEngine
}
});El core no decide de dónde vienen las librerías. Cada consumidor las proporciona.
2. Render puro separado de decoración
El render tiene dos fases:
Separar esas fases permite probar el parser sin DOM completo y probar decoradores con fixtures pequeños.
3. Tema como datos
La configuración de los diagramas dejó de depender de sincronizar CSS con scripts. El core acepta tokens de tema y el producto decide la estética.
| Antes | Después |
|---|---|
| El motor de diagramas leía valores derivados de CSS local | El motor de diagramas recibe tokens explícitos |
| Visor web vendorizado | Visor web consume el core |
| Bugs arreglados tres veces | Bugs arreglados una vez |
La migración no destructiva
No migré todo a la vez. El orden importa porque cada consumidor tiene un riesgo distinto.
| Paso | Objetivo | Verificación |
|---|---|---|
| Paquete local | Tener API estable y tests | Unit tests del core |
| Consumidor interno | Probar consumidor menos crítico | Preview de docs internos |
| Editor | Reemplazar la copia canónica | Demos visuales de Markdown enriquecido |
| Visor web | Eliminar reader vendorizado | Paridad página por página |
| Publicación | Validar nuevo consumidor | Sitio real generado desde Markdown |
TipUna migración que rompe paridad visual no es una migración. Es un bug con envoltorio de arquitectura.
Checklist de paridad que usé
- Headings con IDs estables.
- Código con highlight.
- Diagramas con el mismo tema.
- Fórmulas inline y block.
- Alertas tipo callout.
- Columnas.
- Tablas con estilos.
- Footnotes generadas al final del documento.
- Imágenes con rutas relativas resueltas desde el Markdown.
Qué valida que la decisión fue correcta
La extracción merece la pena cuando cambia la forma de operar:
| Señal | Qué significa |
|---|---|
| Un bug de render se arregla una sola vez | El core tiene dueño real |
| Un consumidor nuevo entra sin tocar el core | El contrato está bien cortado |
| El editor conserva su UI específica | El paquete no absorbió producto |
| El visor web deja de vendorizar | La deuda visible baja |
La prueba más fuerte no es que el paquete exista. Es que un cuarto consumidor lo use sin pedir una excepción rara.
Lo que no haría
- No extraería por el segundo fork si el contrato todavía no está claro.
- No metería i18n, navegación, acciones de copiar o UI del editor dentro del core.
- No mezclaría esta migración con features nuevas del renderer.
- No usaría flags para convertir una librería en cinco productos escondidos.
La regla que me llevo
Una librería compartida no se diseña desde cero. Se descubre cuando el mismo contrato aparece en varios productos y empieza a doler en todos.
Este artículo es parte del trabajo de plataforma que cuento en Sobre mí. Si te interesa el tema, sigue el blog o escríbeme.