Lazy loading is a data-loading technique that contrasts with eager loading. While eager loading involves loading all data at once, lazy loading works on the opposite principle: data is only loaded when it is needed.
This approach is widely used to optimize loading times and application performance. There are various forms of lazy loading, such as:
Implementing lazy loading provides several benefits for user experience and server performance:
In summary, lazy loading helps prevent resource overload by limiting expensive operations, thereby enhancing application performance and responsiveness.
Lazy loading can be implemented in JavaScript using techniques like IntersectionObserver to detect when an element becomes visible in the viewport, and the fetch API to retrieve data as needed.
However, these traditional approaches quickly show limitations in dynamic applications where server interactions are frequent.
The IntersectionObserver API in JavaScript is a common method to detect when an element enters the user’s viewport. It triggers actions (like loading images or components) as soon as an element becomes visible, optimizing the visual rendering.
However, IntersectionObserver is not designed to directly handle dynamic server data. Its role is limited to observing an element’s visibility, and it needs to be combined with the fetch API and Promises to dynamically load server-based content, such as article lists or more complex components.
The fetch API in JavaScript allows you to make HTTP requests to the server to retrieve data as needed, typically in response to user actions. This approach uses Promises to handle asynchronous requests without blocking the user interface.
Basic Workflow of the Fetch API:
Challenges with Using Fetch and IntersectionObserver:
These constraints can quickly make the code complex to maintain, especially in projects involving frequent user interactions and server calls.
Example: Lazy Loading a Modal with IntersectionObserver and Fetch API
document.addEventListener("DOMContentLoaded", function () {
const modal = document.getElementById("detailsModal");
const modalContent = document.getElementById("modalContent");
const loadModalButton = document.getElementById("loadModalButton");
let observer;
// Load data with fetch()
async function loadModalContent() {
try {
modalContent.innerHTML = "Loading...
"; // Display a spinner
const response = await fetch('/path/to/api/articles');
if (!response.ok) {
throw new Error("Error fetching data");
}
const data = await response.json();
modalContent.innerHTML = `${data.title}
${data.content}
`;
} catch (error) {
console.error(error);
modalContent.innerHTML = "Error fetching data.
";
}
}
// Function to observe when the button enters the viewport
function createObserver() {
observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
loadModalContent();
modal.style.display = "block";
observer.disconnect(); // Avoid multiple calls
}
});
});
observer.observe(modalContent);
}
createObserver(); // Initiate the observer when content is visible
});
As seen earlier, it’s possible to use the fetch API in JavaScript to dynamically load data into a modal. However, this requires significant JavaScript code to manage Promises and ensure data is fetched and injected correctly.
With Hotwire, this logic is simplified by using turbo_frame_tag, which makes it easy to implement lazy loading for modal content. Hotwire automatically handles data loading via HTTP requests without the need for extra JavaScript.
Define a route for the modal content. This route will be passed as the src attribute in the turbo_frame_tag.
# config/routes.rb
get :modal_content, to: 'controller#modal_content', as: 'object_details'
Create the modal_content action in the relevant controller. This action prepares the required data and renders the partial to be displayed in the modal.
# app/controllers/your_controller.rb
def modal_content
render partial: 'path/to/your/partial'
end
That’s it for the backend setup ! 🚀
To implement lazy loading with Hotwire on the frontend, we will use turbo_frame_tag in our views. This will allow us to dynamically load the modal’s content at the moment it is opened, without requiring additional JavaScript to handle asynchronous requests.
In the main view, we start by creating a link or button that will trigger the modal’s opening.
Here is an example:
# In the view where we'll display the modal
# Button to display the modal
<%= link_to '#',
class: 'btn btn-bg-red text-light my-1 rounded-circle d-flex justify-content-center align-items-center',
type: 'button',
style: "width: 50px; height: 50px;",
data: {
bs_toggle: 'modal',
bs_target: "#detailsObject#{@object.id}",
} do %>
<%= bootstrap_icon "search", width: 25 %>
<% end %>
# The modal
<div class="modal fade" id="detailsObject<%= @object.id %>" tabindex="-1" aria-labelledby="detailsObjectLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<!-- Here, we'll add the turbo_frame_tag -->
</div>
</div>
</div>
<div class="modal fade" id="detailsObject<%= @object.id %>" tabindex="-1" aria-labelledby="detailsObjectLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<!-- Hotwire -->
<%= turbo_frame_tag 'modal',
src: modal_content_path(id: @object.id),
loading: "lazy" do %>
<!-- Spinner -->
<div class="d-flex justify-content-center align-items-center my-2">
<div class="spinner-border mb-5 mt-5" style="width: 5rem; height: 5rem;" role="status"></div>
</div>
<% end %>
</div>
</div>
</div>
Finally, we define the partial rendered by our controller action. This partial contains the dynamic content of the modal and is wrapped in a turbo_frame_tag with the same ID.
This ensures that the content loads into the correct container in our view.
<%= turbo_frame_tag 'modal' do %>
<!-- Modal content here -->
<% end %>
In conclusion, Hotwire allows us to implement lazy loading in a simpler way than using IntersectionObserver and the Fetch() API. Furthermore, this implementation allows us to better respect the MVC pattern of Rails and its conventions by adhering to the strategy:
Back-End manages the construction of data as well as any server requests.
Front-end manages the display and layout of data coming from our Back-end.