“Buy or make” dilemma can be hard in frontend development: the 360 Product View case

We dived into the development of a very common frontend feature and realised we couldn’t use any of the available libraries. What went wrong and what went right in our experience.

Modern users expect more and more from content and their experience interacting with it. Static images, while still being important, are not driving enough engagement to users and are surpassed by interactive experiences such as videos and multi-point-of view experiences.
One of the simple content-interactions users are used to have is the ability to see a product in detail from all angles while choosing whether to buy it or not (we call it 360 product view).

It’s a very common objective and we didn’t expect to build our own solution for this, there are many ready-to-use solutions but, in the end, we couldn’t find any suitable one.

Example of 360 product view

Our product includes a web component (usually referred as “the player”) that is able to manage different content types (audio, video, pictures, documents, galleries, etc), it is modular and can be easily extended or personalized by any web developer with average Javascript knowledge.

Our goal was to integrate in our “player” the rendering a 360degree product view feature in a seamless way. There were some key requirements:

Mandatory:

  • Compatible with Chrome, Firefox, Safari, Edge and IE9+. Yes, some companies still use that.
  • Compatible with mobile browsers (at least chrome and safari latest releases).
  • Personalisation has to be consistent player’s one: our player can be easily customized by providing CSS and JS modules through a dedicated area in our product, this has to propagate to the 360 product view too.

Nice to have:

  • zoom the content (at least or zoom or rotate something like this)
  • pan when content is zoomed (like this)

Buy or make?

Our initial thought was: “hey, out there there has to be a library that perfectly fits our needs. We will pick it, adapt it and reach objective with low effort!”. 
We then begun our search and found a lot of libraries / plugins that were handling product views, less than we thought but still a good amount.

Quickly enough we discarded the majority of those libraries for various reasons: some were just plain ugly, some were working ok but they felt “old” and the majority of them didn’t fullfil all the requirements (most were not allowing the zoom feature and I did want to have it in our Product View, because details matter when looking at a product you want to buy). 
The remaining libraries were just:

Some of those are not free (which is not an issue but if we pay for it, we expect quality from it) and all of them would have required us to perform tweaks to support our customization class hierarchy.
The first library we explored was Spritespin, it looked fine but it didn’t have zoom and rotate-while-zooming features out of the box, so we moved to Magic360. Magic360 was better but still missed the same features and it also has a commercial license only. The last one, Ajax-zoom, had everything we wanted and it looked pretty cool. Unfortunately there were 2 issues here:

  • It requires backend functionality. This means that we need to plan for both frontend and backend integration, adding a new dependency on our player component on devops/backend compentences, causing increased complexity and maintenance costs in the future;
  • It only has a commercial license, adding license cost to the integration ones .

Try to make

A quick estimate about the time required to deeply evaluate those libraries, include them in our player and extend it made us also consider the following option: “maybe we can take the best from these libraries and implement a bespoken solution, perfect for our needs and it would take about the same amount of time.”

So we created few proof of concepts with different techniques to understand how difficult it would be to implement such component and estimate the time it would take to fully implement it. 
The first POC was to evaluate whether we should just append a lot of images and put a drag listener to show and hide the right ones or put just one image and change the source attribute through the drag listener.

Both resulted in poor and glitchy behaviour in some browsers. It also raised a problem with our default image caching: the service we use to return the images is a serverless real time image editor providing images with 10 minute cache; this means that if a user stays in the page for more than 10 minutes and then he interacts with the product view, the browser will download each frame as needed resulting in a very bad experience, unless we pre-fetch all the frames again.

So we changed approach: use a canvas to display the current frame. 
With this technique we download every image at the initialization time and then draw the first frame in the canvas; there is still a drag listener that draws a different frame based on how far the user moves the mouse. 
Surprisingly, this resulted in a very good and smooth experience with no cache problems and was very quick to implement.

Supported by such good result we pursued the idea of implementing our own 360degree component.
We were able to achive every requirement we set, compatible with every browser altought with some challenge in enabling zoom, pan when zoomed and rotate when zoomed features.

The key challenges that we encountered were:

  • Making it fully compatible with IE9–10: to implement the pan and zoom features we used a jquery plugin; one of its drawbacks is that this plugin listens and ‘absorbs’ almost any user event that is triggered in the DOM tree where is instantiated; to overcome this problem we put a DOM element above the one that manages the zoom and we listen for every event there, filtering and proxying events to the lower jquery layer: we process just the click or touch events to rotate the content and let the other events bubble up to the zoom handler.
    To make it fully compatible with IE9–10 we had to create this layer as a fully transparent image in a img tag instead of a simple div or span because those browsers have a bug that prevents some events from firing on transparent divs and spans.
  • Adapt image quality when the window resizes: when the player is instantiated we download images with the exact size of the div where they are created, this optimizes the bandwidth required and maximises quality of image. This, unfortunately, also leads to the need to manage image size/quality when the user enlarges the browser or when he sets the player in full screen. To prevent visible artifacts, when we detect a resize event, we start downloading in background the bigger frames and when each donwload session finishes we replace the previous image with the higher quality one.
  • To optimize performance we call requestAnimationFrame to draw in the canvas only when the user is interacting with the content, this is a nice practice to improve performance, battery usage and responsiveness of the host page

Not reusing an existing library allowed us to create a model that easily fits into the existing player codebase and it was very easy to implement the customization features by reusing the player’s existing approach.

There are, of course, some drawbacks in our implementation too

In the current implementation, the player downloads every frame on the first user interaction thus causing, especially if content is composed by lots of images, more wait time than desired (similar to this). 
This is mitigated by the fact that, thanks to our real time image editing service: we generate the images at the exact size needed for the rendering by using just the needed bandwidth and nothing more. Still, it’s not as good as a “streaming” approach. 
We saw that some of the libraries, especially when the user zooms in, were using a backend service to receive just portions of images to minimize the number of KB that the user had to download, we could have used the real-time image editor features to achieve the same freature but unfortunately we couldn’t implement this feature and meet the deadline as well, so we plan to optimize this behavior in the future.

Another similar issue is that if the user creates a product view using frames with transparency (e.g. 60 PNG as frames) it will probably be a slow loading one. This is due to transparent PNG’s being heavier than Jpg’s. 
We mitigate this, again, by using our real-time image editor: images are returned as png’s only if they really have transparency, otherwise they will be converted to jpg and cached for 1 year. This completely removes the problem when the content has png’s without transparency, but doesn’t improve performance on transparent 360 degree product view frames.

This project has been released in production already and is being used by customers and partners right now. We are pretty happy about it, especially because the customization is seamlessly integrated with the rest of the player components.

We would like to improve it in the future applying the backend support to improve the load/zoom performance

So what did we learn during this journey?
In analysis step we were sure that we were going to find something perfect for our needs, being able to shorten development times. By doing this we underestimated our requirements and this taught us to do a better analysis on requirements and to scout a bit earlier for technologies to understand whether it’s better to create our own or buy something ready to use. The challenge lies in the ability (or inability) to quickly estimate the effort to perform the scouting on existing solutions and estimate “make time”, we used a timeboxed approach to fix the amount of time we could dedicate to this research but we still feel this could have been managed in a more efficient way.

At the end, developing our own component might feel like reinventing the wheel but gave us a chance to optimize for our specific requirements and achieve the best result possibile for the end user, providing consistend apis and models that also helps partners in their learning process.