Compilación en Dart: JIT, AOT y los compiladores de JavaScript
Dart no se ejecuta de una sola forma; utiliza distintos pipelines de transformación de código fuente a instrucciones ejecutables dependiendo de tu objetivo. Esta dualidad existe para resolver el conflicto entre la velocidad de iteración del desarrollador y el rendimiento del usuario final. Los tres modos de compilación son los distintos caminos que toma tu código: JIT (Just-In-Time), AOT (Ahead-Of-Time) y la compilación a JavaScript.
El modo JIT, que es el que utilizas cuando ejecutas dart run, depende de la Dart VM. Aquí, el código se compila mientras se ejecuta, lo que permite el Hot Reload al reinyectar cambios en la memoria sin reiniciar el proceso. Esta flexibilidad tiene un coste: la VM debe dedicar ciclos de CPU al profiling (observar qué partes del código se ejecutan más) para aplicar optimizaciones dinámicas.
Por el contrario, el modo AOT es el estándar para entornos de producción y CLIs. Con dart compile exe, el código se transforma en un binario nativo antes de la ejecución. Esto elimina el overhead de la compilación en runtime, resultando en un startup time mucho más rápido y un uso de memoria más eficiente, aunque pierdes la capacidad de modificar el código “en caliente”.
Cuando el destino es el navegador, nos encontramos con dos compiladores distintos. dart2js es el optimizador para producción que aplica un tree shaking agresivo para eliminar código que no se usa, reduciendo el tamaño del bundle. dartdevc es el compilador incremental diseñado para desarrollo, priorizando que el cambio en el código se refleje casi instantáneamente en el navegador.
Todo este ecosistema se apoya en un Garbage Collector (GC) generacional. Al segmentar la memoria en generaciones (objetos jóvenes de vida corta vs. objetos viejos), Dart minimiza las pausas de latencia, permitiendo que la ejecución sea fluida tanto en un servidor de alto rendimiento como en una interfaz de usuario interactiva.
import 'dart:async';
/// Representa una carga de trabajo intensiva para observar el comportamiento
/// del runtime durante las fases de calentamiento y ejecución estable.
int procesarDatos(int iteraciones) {
int acumulador = 0;
for (int i = 0; i < iteraciones; i++) {
// Operación matemática simple para evitar optimizaciones excesivas
// de compiladores muy agresivos en ejemplos triviales.
acumulador += i;
}
return acumulador;
}
void main() async {
print('Iniciando ejecución...');
final cronometro = Stopwatch()..start();
// 1. Fase de "Warm-up" (Calentamiento)
// En modo JIT, la VM detecta este loop repetitivo y lo optimiza en runtime.
// En modo AOT, este código ya ha sido optimizado por el compilador estático.
for (int i = 1; i <= 3; i++) {
final inicioLoop = Stopwatch()..start();
procesarDatos(5_000_000);
print('Calentamiento fase $i: ${inicioLoop.elapsed}');
}
// 2. Fase de Steady State (Estado Estable)
// El punto donde el rendimiento se estabiliza tras las optimizaciones.
print('Iniciando carga pesada...');
final resultado = procesarDatos(50_000_000);
stopwatchFinalizar(cronometro, resultado);
}
void stopwatchFinalizar(Stopwatch sw, int res) {
sw.stop();
print('Resultado final: $res');
print('Tiempo total de ejecución: ${sw.elapsed}');
}
// Nota: No puedes usar dart:mirrors si planeas compilar con AOT.
En el programa anterior, la función procesarDatos es el punto focal de la optimización. Si ejecutas esto en modo JIT (desarrollo), notarás que las primeras llamadas en el bucle de “calentamiento” pueden ser ligeramente más lentas que las últimas; esto sucede porque la Dart VM está analizando el comportamiento del bucle para decidir cómo optimizarlo mediante el profiling.
Cuando llegas a la fase de Steady State con el valor de 50_000_000, el motor ya ha identificado que procesarDatos es una función “caliente” (hot) y ha generado una versión altamente optimizada en máquina. En un binario compilado con AOT, este proceso de “aprendizaje” es inexistente porque el compilador ya realizó el trabajo de optimización de forma estática, lo que garantiza que el tiempo de respuesta sea consistente desde el primer microsegundo.
El uso de Stopwatch nos permite ver la diferencia entre la latencia inicial y el rendimiento sostenido. En un entorno de producción (AOT), el startup time sería prácticamente inmediato, mientras que en JIT, hay un pequeño periodo de “arranque” mientras el compilador JIT se ajusta.
El error frecuente
Un error clásico ocurre al intentar utilizar dart:mirrors (reflexión) en proyectos que se planean desplegar en modo AOT o como binarios nativos.
// Esto compilará sin problemas en desarrollo (JIT)
import 'dart:mirrors';
void checkType(Object obj) {
print(reflectClass(obj));
}
void main() {
checkType("Hola Dart");
}
Si intentas compilar este código con dart compile exe, el compilador fallará. Esto se debe a que la reflexión requiere la capacidad de inspeccionar metadatos en tiempo de ejecución, algo que el modo AOT elimina por completo para reducir el tamaño del binario y maximizar el rendimiento. Para producción, debes diseñar tu arquitectura basándote en code generation (usando build_runner) en lugar de reflexión.
N° 3