Arsalan Khattak
30 August 2024

How to lazy load data with the Bryntum Scheduler

Lazy data loading is a new feature in the Bryntum Version 6 release of the Scheduler and Grid components. It […]

Lazy data loading is a new feature in the Bryntum Version 6 release of the Scheduler and Grid components. It improves Bryntum Scheduler performance by only loading data as needed. Instead of loading all the data at once, data is loaded in chunks as a user scrolls down or across the Scheduler—especially beneficial for applications with large amounts of data.

This tutorial will show you how to add lazy loading to an existing full-stack Bryntum Scheduler app that uses an SQLite database as a data store. You can use the lazy loading feature with any database of your choice. We’ll populate the database with a large dataset and have the Scheduler app load the data simultaneously. Then, we’ll add lazy loading so the app only fetches data as needed and see how performance improves.

At the end of this tutorial, you’ll have a Bryntum Scheduler with lazy loading on scroll:

Visit our website’s live demo of the lazy loading feature in Bryntum Scheduler.

Getting started

We’ll start with a basic full-stack Bryntum Scheduler app with a separate backend and front end. The front end uses vanilla JavaScript, and the backend server is an Express app.

The backend server has REST API endpoints that we’ll use to get data from a local SQLite database. We’ll populate the SQLite database using data from JSON files.

Set up the Express backend app

Clone the Express server starter GitHub repository. The added-lazyloading branch contains the code for the completed tutorial.

The Express server has three API routes: "/read-resources", "/read-events", and "/read-resourcetimeranges". The Bryntum Scheduler in the frontend will make fetch requests to these API routes to get the resources, events, and resource time ranges data from the database.

The server uses Sequelize as an ORM to perform CRUD operations on the database and model the data. Sequelize is configured to use a local SQLite database, and the data models are in the /model folder.

The addExampleData.js file uses the data models and the example JSON data in the initialData folder to add data to the local SQLite database.

Now, let’s create a SQLite database file and populate it with the example JSON data.

Populate the SQLite database with example data using the Node.js script

First, install the dependencies of the Express server starter:

npm install

We’ll use the code in the addExampleData.js file to create a local SQLite database file and populate it with the example data.

In this file, the Sequelize instance in the /config/database.js file is imported and configured to connect to a SQLite database. We set the SQLite database to be stored in the project root folder and named the database file database.sqlite3.

The addExampleData.js script uses the Sequelize instance to create the database, connect to it, and run queries to add the example data.

Before the example data is added to the database, the sequelize.sync() method is called, which creates the database tables if they don’t exist for the data models.

The addExampleData() function is then called. This function reads the data in the initialData folder using the readFileSync method of the Node.js File System module.

A transaction and the bulkCreate methods are then used to create and insert multiple records using minimal database queries.

If the database.sqlite3 file does not exist in the root directory; it will be created when this code is run.

Now run this Node.js script using the following command:

node addExampleData.js

You should see a database.sqlite3 file created in your root folder. This database is populated with the example large dataset.

Run the local development server for the backend server app using the following command:

npm start

Go to http://localhost:3000/read-resources and http://localhost:3000/read-events to see the loaded dataset in the browser.

We’ll now set up the frontend repo with a Bryntum Scheduler.

Set up the frontend app

Clone the Bryntum Scheduler starter GitHub repository. Note that the added-lazyloading branch contains the code for the completed tutorial. The starter repository uses Vite as a development server and JavaScript bundler.

Install the Vite dev dependency by running the following command:

npm install

Now install the Bryntum Scheduler component using npm. First, get access to the Bryntum private npm registry by following the guide in our docs. Once you’ve logged in to the registry, install the Bryntum Scheduler component. Ensure you install Version 6 or higher, as the lazyLoad feature we’ll use was released in Version 6.

Run the local development server for the Bryntum Scheduler app using the following command:

npm run dev

Now navigate to http://localhost:5173/. You should see a Bryntum Scheduler with a large dataset loaded from our SQLite database:

Preview of Bryntum Scheduler

The Scheduler takes a while to load the data, as the example dataset is large. Once we’ve enabled the lazy loading feature in the Bryntum Scheduler, we’ll use the “Network status” indicator at the top left in the toolbar to indicate when data is lazy loaded.

Let’s add lazy loading to improve the Scheduler’s performance by only loading data as needed.

Add the lazyLoad feature to the Scheduler

In the main.js file in the frontend app, you’ll see that the Scheduler is configured to use three separate data stores: resourceStore, eventStore, and resourceTimeRangeStore. Add the following properties to each of these data stores:

    lazyLoad: true,
    listeners: {
      lazyLoadStarted() {
        updateNetworkValue("Loading", "blue");
      },
      lazyLoadEnded() {
        updateNetworkValue();
      },
    },

Remove the autoLoad: true, property from eventStore and resourceTimeRangeStore.

We enable the lazy load functionality for each data store by setting lazyLoad it to true. We also use the two lazyLoad event listeners to update the network status at the top left of the Scheduler in the toolbar. When data is lazy loaded, the network status will be updated to “Loading”.

You can also configure the chunkSize of the lazyLoad feature, which is the number of records fetched for each lazyLoad request. Replace lazyLoad : true with the following:

    lazyLoad: {
      chunkSize: 100, // default value
    },

In the features config object, add the group property and set it to false as grouping is not supported when using a lazy loading store. By default, the group property is set to true so we need to disable it.

    group: false,

Note that infiniteScroll is enabled. It’s essential to enable this feature when using lazy loading so that the timeline is infinitely scrollable.

Now, we need to update the backend API endpoints to fetch only the required data and return the expected response for a lazy-loading data store.

Modify the read API endpoints to get and send chunks of data for lazy loading

We need to update our read API endpoints for lazy loading to work. In the server.js file in the backend app, replace the "/read-resources" API GET request route definition with the following:

app.get("/read-resources", async (req, res) => {
  try {
    const startIndex = parseInt(req.query.startIndex) || 0;
    const count = parseInt(req.query.count) || 100;
    const resources = await Resource.findAll({
      offset: startIndex,
      limit: count,
    });
    res.json({
      success: true,
      data: resources,
      startIndex,
      count,
      total: await Resource.count(),
    });
  } catch (error) {
    console.error("Failed to fetch resources:", error);
    res.status(500).json({
      success: false,
      message: "Failed to fetch resources",
    });
  }
});

We get the startIndex and count from the request query parameters. When the Bryntum Scheduler lazy loading resources store requests data, it adds a startIndex and a count URL parameter to the request. The startIndex is the index where the server should start reading data from the database. The count parameter is equal to the chunkSize, which is the number of records to fetch from the database, starting from the startIndex.

We call the Sequelize findAll method on the Resources data model to retrieve all the records from the resources table in the SQLite database. We restrict the SQL query to only get count number of records, starting from the startIndex. We use the offset and limit properties of the findAll method options config to achieve this.

Next, replace the "/read-events" API GET request route definition with the following:

app.get("/read-events", async (req, res) => {
  try {
    const startIndex = parseInt(req.query.startIndex) || 0;
    const count = parseInt(req.query.count) || 100;
    const startDate = req.query.startDate
      ? new Date(req.query.startDate)
      : new Date(0);
    const endDate = req.query.endDate
      ? new Date(req.query.endDate)
      : new Date();
    // Query the database for resources to find matching resource IDs
    const resources = await Resource.findAll({
      attributes: ["id"],
      offset: startIndex,
      limit: count,
    });
    const resourceIds = resources.map((resource) => resource.id);
    // Query the database for events using filtered resource IDs and date range
    const events = await Event.findAll({
      where: {
        resourceId: {
          [Sequelize.Op.in]: resourceIds,
        },
        [Sequelize.Op.or]: [
          { startDate: { [Sequelize.Op.between]: [startDate, endDate] } },
          { endDate: { [Sequelize.Op.between]: [startDate, endDate] } },
        ],
      },
    });
    res.json({
      success: true,
      data: events,
      total: events.length,
    });
  } catch (error) {
    console.error("Failed to fetch events:", error);
    res.status(500).json({
      success: false,
      message: "Failed to fetch events",
    });
  }
});

Replace the "/read-resourcetimeranges" API GET request route definition with the following:

app.get("/read-resourcetimeranges", async (req, res) => {
  try {
    const { query } = req;
    const startIndex = parseInt(query.startIndex) || 0;
    const count = parseInt(query.count) || 100;
    const startDate = query.startDate ? new Date(query.startDate) : new Date(0);
    const endDate = query.endDate ? new Date(query.endDate) : new Date();
    // Query the database for resources to find matching resource IDs
    const resources = await Resource.findAll({
      attributes: ["id"],
      offset: startIndex,
      limit: count,
    });
    const resourceIds = resources.map((resource) => resource.id);
    // Now fetch the time ranges for these resources that match the date constraints
    const timeRanges = await TimeRange.findAll({
      where: {
        resourceId: {
          [Sequelize.Op.in]: resourceIds,
        },
        [Sequelize.Op.or]: [
          {
            startDate: {
              [Sequelize.Op.between]: [startDate, endDate],
            },
          },
          {
            endDate: {
              [Sequelize.Op.between]: [startDate, endDate],
            },
          },
        ],
      },
    });
    res.json({
      success: true,
      data: timeRanges,
      total: timeRanges.length,
    });
  } catch (error) {
    console.error("Failed to fetch resource time ranges:", error);
    res.status(500).json({
      success: false,
      message: "Failed to fetch resource time ranges",
    });
  }
});

Now we need to import the Sequelize class as we use Sequelize operators in the API routes.

import { Sequelize } from 'sequelize';

Run the backend and frontend development servers and go to http://localhost:5173/. You’ll see the Bryntum Scheduler with the SQLite data lazy-loaded into the data stores.

Note that lazy loading occurs when scrolling vertically or horizontally. The “Network status” indicator in the toolbar’s top left changes to “Loading” when lazy loading is in progress. You can also look in your browser dev tools Network tab for each lazy loading request.

If you scroll down fast enough, you’ll see that rows still being loaded are rendered with a “skeleton” placeholder. When a user scrolls down the Scheduler, the lazyLoad feature initiates a new load request for data when the end of the loaded data is nearly reached. The load request is a promise that resolves to the loaded records. While the promise is unresolved, the row is rendered with a “skeleton” placeholder.

In this video example, the connection has been set to Slow 3G, so you can see the skeleton placeholder while the data is being loaded.

Next steps

Now that you know how to add the new lazyLoad feature to Bryntum Scheduler, try adding sorting and filtering. Note that local sorting and filtering are unsupported when a store uses lazy loading. To use the built-in Bryntum Scheduler sorting and filtering features, do the sorting and filtering in the backend. Set the filterParamName and sortParamName properties in the AjaxStore to pass the filters and sorters to the backend.

If you have any questions or suggestions for future improvements, contact us in our forums. Bryntum provides a 45-day trial to explore the full capabilities of our tool in your project. Additionally, check out our live demos.

Arsalan Khattak

Bryntum Scheduler