La misión de Webflow es poner superpoderes de desarrollo al alcance de todos.
En agosto de 2023 lanzamos nuestras primeras API del Diseñador que permitieron a los desarrolladores interactuar con el Diseñador de Webflow. Los desarrolladores comenzaron de inmediato a crear potentes aplicaciones para que los usuarios de Webflow pudieran crear sitios más rápidos y con más funciones. Con el lanzamiento de estas API, empezaron a llegar solicitudes para crear nuevas funcionalidades y nos dimos cuenta de que, para aprovechar todo el potencial del Diseñador, necesitábamos cambiar nuestra forma de estructurar las API. Esta reestructuración (V2) trajo consigo algunos desafíos de rendimiento que se resolvieron con un enfoque multifacético.
API del Diseñador V1 vs V2
Como comentamos en la parte 1, las API actuales del Diseñador (V2) están diseñadas para permitir una comunicación inmediata con nuestra lógica de aplicación interna para cada solicitud de API, una capacidad que no estaba disponible en nuestras API V1. A continuación, un ejemplo que muestra cómo escribir el mismo código tanto para V1 como para V2:
// Setting a custom attribute
// V1
const el = await webflow.getSelectedElement();
el.setCustomAttribute('data-id', '123');
await el.save()
// V2
const el = await webflow.getSelectedElement();
await el.setCustomAttribute('data-id', '123');
Cambiamos el enfoque sobre cómo persistíamos los datos para todas las API, incluidos elementos, páginas y estilos. Al ejecutar las API V1, las actualizaciones se almacenaban en la memoria y no se guardaban en el Diseñador de Webflow hasta que se llamaba a la API save(). Esto hacía que trabajar con objetos de forma masiva fuera más eficiente, pero conllevaba bastantes inconvenientes:
- No podíamos validar inmediatamente la entrada de las API. Esto significaba que los fallos no se detectaban a tiempo y teníamos que restringir ciertas funciones para evitar problemas de seguridad.
- Aprovechar todo el potencial de la lógica interna del Diseñador no era posible. Por ejemplo, en la V1 no podías crear ningún tipo de elemento como sí puedes en la V2 usando los Preajustes de elementos.
En última instancia, estábamos limitados por las API que podíamos construir usando la arquitectura V1.
Optimizaciones iniciales
A medida que los desarrolladores migraban de la V1 a la V2, empezamos a recibir informes sobre problemas de rendimiento al realizar operaciones masivas, como insertar docenas de elementos en el lienzo o actualizar todos los valores de metadatos de una página simultáneamente. Las API V2 cambiaron la forma en que se gestionaban las solicitudes; en lugar de agruparlas y enviarlas una vez por conjunto de operaciones como en la V1, la aplicación enviaba ahora una solicitud por cada llamada a la API. Como resultado, tuvimos que optimizar el manejo de las operaciones masivas en la V2 para igualar el rendimiento de la V1.
Comenzamos nuestra investigación de rendimiento centrándonos en las API que tenían un uso elevado y un alto coste de rendimiento, especialmente en sitios grandes. Estas API eran nuestras API de inserción de elementos (append, prepend, before y after).
Empezamos analizando cada parte del ciclo de vida de la API utilizando la API performance.now. Desglosamos cada fragmento de lógica y descubrimos que el sistema iteraba innecesariamente sobre todos los elementos de la página para localizar el elemento objetivo, y seguía iterando incluso después de haberlo encontrado. La razón era una función reutilizada que nunca se optimizó para este caso de uso específico.
La actualización del código quedó así:
// Before
const addElementAtAnchor = (page, newElement, placementPosition) => {
const paths = findPaths(page, el => el.id === newElement.id);
const path = paths[0];
return addElementAtPosition[placementPosition](page, path, newElement);
};
// After
const addElementAtAnchor = (page, newElement, placementPosition) => {
const path = findPath(page, el => el.id === newElement.id);
return addElementAtPosition[placementPosition](page, path, newElement);
};
Introdujimos un cambio pequeño pero impactante al crear una nueva función, findPath, que deja de iterar tras encontrar la primera instancia de un elemento en la página. Esta optimización redujo el número de iteraciones, lo que resultó en una disminución del 50% en el tiempo de ejecución de la API (incluso más en sitios grandes). Aunque fue una mejora significativa, notamos que la API seguía siendo más lenta de lo ideal cuando se invocaba varias veces seguidas. Cada llamada a la API promediaba ahora unos 20 ms, pero insertar docenas de elementos a la vez seguía siendo más lento en comparación con nuestra API V1.
Análisis adicional
Tras la optimización inicial, dimos un paso atrás para considerar todo el ciclo de vida de la API. Con las API V2, ahora nos comunicamos con el Diseñador cada vez que se llama a una API. Esto conlleva intrínsecamente tres fuentes principales de sobrecarga.
- Comunicación postMessage Las API del Diseñador utilizan la API de JavaScript window.postMessage para enviar y recibir datos entre el Diseñador y la aplicación.
- Procesamiento del estado de la aplicación Cada API también actualiza nuestro estado interno de Flux (nuestra versión interna de Redux) para que los datos puedan persistirse en la base de datos.
- Ejecución de lógica Las API tienen su propio conjunto de lógica para procesar la carga útil.
Para obtener una base de referencia de esta sobrecarga, medimos cada fuente para determinar cuánto tiempo tardaban en procesarse. Utilizamos las API performance.now y console.time para obtener una duración promedio de cada parte de nuestra lógica de API.
Medición de la comunicación postMessage
Para determinar cuánto tarda la API promedio en enviar y recibir una respuesta, medimos el tiempo de solicitud y respuesta de postMessage entre la aplicación y el Diseñador.
// We executed this in the Designer window.
// This script listens for the postMessage request from the App.
window.addEventListener('message', e => {
if (e.data === 'ping') e.source.postMessage('pong', '*');
});
// These were executed in the App window.
// This script listens for the response from the Designer
window.addEventListener('message', e => {
if (e.data === 'pong') console.timeEnd('round-trip');
});
// This script sends the request, similar to how our APIs work
(() => {
console.time('round-trip');
window.parent.postMessage('ping', '*');
})();
Throughout this process, we observed that the duration of postMessage varied based on factors such as the browser’s current memory usage and the state of the JavaScript event loop. On average, this stabilized around 4ms per request when tested on the latest versions of Chrome and Firefox. While this didn’t raise any immediate concerns, we will continue monitoring Designer memory usage to ensure this average is maintained.
Durante este proceso, observamos que la duración de postMessage variaba según factores como el uso de memoria del navegador y el estado del bucle de eventos de JavaScript. En promedio, esto se estabilizó en unos 4ms por solicitud al probarlo en las versiones más recientes de Chrome y Firefox. Aunque esto no generó preocupaciones inmediatas, seguiremos monitoreando el uso de memoria del Diseñador para asegurar que este promedio se mantenga.
We use a Flux architecture for storing state across the entire Designer. This follows similar patterns to Redux, where application code can dispatch actions then a reducer processes that action and returns the updated state. Designer APIs perform their own reducer logic and then we dispatch an action to commit the state. Therefore we wanted to understand what the baseline duration was from an action being dispatched to the reducers to be done processing.
const t0 = performance.now();
dispatch({type: 'API_EXECUTION'});
fluxState.onChange((action) => {
if (action === 'API_EXECUTION') {
const t1 = performance.now();
console.log(`Flux processing time: ${t1 - t0}ms`);
}
});
Procesamiento del estado Flux
Logic Execution
Utilizamos una arquitectura Flux para almacenar el estado en todo el Diseñador. Esto sigue patrones similares a Redux, donde el código de la aplicación puede enviar acciones y luego un reductor procesa esa acción y devuelve el estado actualizado. Las API del Diseñador ejecutan su propia lógica de reductor y luego enviamos una acción para confirmar el estado. Por lo tanto, queríamos entender cuál era la duración base desde que se envía una acción hasta que los reductores terminan de procesarla.
Element Builder API
Al medir el tiempo que tarda en procesarse una acción, descubrimos que el promedio era de unos 5ms. Esta es un área que planeamos optimizar más adelante, ya que beneficiaría no solo a nuestras API del Diseñador, sino al Diseñador de Webflow en su conjunto. Aunque para esta iniciativa inmediata, decidimos abordar otras áreas de oportunidad primero.
// Get Selected Element
const selectedElement = await webflow.getSelectedElement()
// Build an element
const rootElement = webflow.elementBuilder(webflow.elementPresets.DOM)
// Append a child element
const childElement = rootElement.append(webflow.elementPresets.DOM)
// Add root element to the designer by appending to selected Element
await selectedElement.append(rootElement)
We also added the ability to set attributes and styles with this new element builder API, helping to dramatically improve API execution time. With this change, we saw that using the element builder API was just as fast as its V1 predecessor.
Ejecución de lógica
In addition to our new element builder API, we also released an API for inserting multiple page metadata values at once: page.setMetadata. This allows developers to make bulk updates to a page all in one API call.
const page = await webflow.getCurrentPage();
await page.setMetadata({
name: 'About,
description: 'All about myself',
isDraft: true,
});
Finalmente, cada API realiza su propio conjunto de lógica especializada. Empezamos a analizar cada API para determinar si podíamos implementar alguna optimización. Pero, al dar un paso atrás, nos dimos cuenta de que, con los hallazgos anteriores, siempre tendríamos algo de sobrecarga en cada API. Esta sobrecarga es un efecto secundario de nuestra arquitectura postMessage, que nos permite tener un bucle de retroalimentación constante entre una aplicación y el Diseñador. Por ello, decidimos aceptar esta sobrecarga y buscar otro camino para acercarnos al rendimiento de la V1.
In addition to the above API, we recommended that developers use the Promise.all API when performing bulk API operations. This helps to run all Designer Extension API requests concurrently, dramatically reducing the time it takes to execute these APIs.
API de creación de elementos
const el = await webflow.getSelectedElement();
await el.setCustomAttribute('data-id', '123');
await el.setCustomAttribute('foo', 'bar');Using Promise.all:
const el = await webflow.getSelectedElement();
await Promise.all([
el.setCustomAttribute('data-id', '123')
el.setCustomAttribute('foo', 'bar')
]);Tras analizar cada parte del ciclo de vida de la API, descubrimos que teníamos una duración base de ~10ms para la ejecución de cualquier API (ejecución de postMessage + procesamiento Flux). Para eliminar esta duración acumulada cuando las API se ejecutan en serie, creamos la API de creación de elementos. Esta API se inspira en la V1, donde construimos un árbol de elementos en memoria y solo lo sincronizamos una vez que el elemento se añade a uno existente en el lienzo.
Próximos pasos
As developers continue to build new Apps, we are keeping an eye on the APIs being leveraged and how we can help make them even faster. In the coming months, we plan to ship APIs for working with page metadata in bulk as well as additional APIs for working with custom attributes. We also are working on automated ways to track performance across all APIs so that we can be proactive in our performance journey. Performance work is never done, but by having a good foundation and direction for our APIs, we believe we can give both developers and users the best possible experience.









.jpeg)
