background
Recently the boss came to a new demand, geographical location group. One of the features is that users can go to the map page, see their current location, scan other players in and out of the circle, and display their avatar on the map page to indicate where they are. The boss said that the map should be able to display 200 players’ heads at once and the page should flow smoothly. Android6 mobile phone, in drag, zoom in, can not appear obvious lag, must ensure user experience. When dragging, zooming, or zooming in to change the map’s viewable range, or staying longer than 60 seconds, you need to update the user’s avatar on the current map.
Train of thought
The requirements are not complicated, but when it comes to fulfilling the boss’s requirements, they are not. Android 6 model, this should be a few years ago thousand yuan machine. The hardware of this model is already very poor, so drawing 200 heads on a map at once would be very difficult. And after dragging and dropping the map, clearing out the 200 avatars already drawn and redrawing the 200 avatars, this will definitely cause the page to appear obvious lag. You have to think carefully about what you’re going to do to meet your boss’s requirements.
Partial drawing
Browsers have a limit on the number of concurrent requests for the same domain name, usually no more than eight. Even if 200 image requests are sent at a time, they are returned in batches. And with that, I can also draw in batches, 200 heads, and I can do 40 batches, and I can only draw 5 at a time, and when the last 5 is done, I can start to draw the next 5. With this in mind, go to the PM and explain to him the browser request limits and performance considerations, can not display all at once, can display slowly. PM accepted this approach, but preferred to show the avatar inside the circle.
In order to get to the point where you can draw in batches, and the things in the circle have to be drawn first, it’s easy to think of priority queues, high-priority first-out queues. In this case, the heads inside the circle have higher priority than the heads outside the circle. Draw 5 heads at a time from the queue until the queue is empty. A synchronous loop that calls each batch of draws until the queue is empty would surely cause the current frame to take longer than 16ms to execute for so long that the browser would block and no other user events would be answered, indicating that the page was dead, which would not work. By using the event loop of javascript, 5 heads can be drawn each time, and such a function can be wrapped in a task. The 200 heads can be divided into 40 such tasks, and then each task is added to the execution queue of javascript respectively. In this way, the avatars can be drawn asynchronously in batches, and the page will not appear stuck.
// pseudo-code,
// Wrap 5 heads at a time into a task and place it in the Event loop
const PER_COUNT = 5
function paintMarker(type) {
// Get the circle data first
const userData = getUserData(type)
if(! userData.length) {if (type= = ="nearbyMarker") {
// When the circle is finished, continue to draw the circle outside
this.engine.pushDraw((a)= > {
this.paintMarker("externalMarker")})}return
}
// Draw 5 at a time
let start = 0
let notAvailable = false
while (start < PER_COUNT && start < userData.length) {
const user = userData[start]
// Fetch one from the instance pool
const marker = this.pools.take()
if(! marker) { notAvailable =true
break
}
// Start drawing your avatar
marker.draw(user)
start = start + 1
}
// Add the next drawing task to the Event loop
if(! notAvailable) {this.engine.pushDraw((a)= > {
this.paintMarker()
})
}
}
Copy the code
Repeated use
For map pages, the upper limit for drawing is 200 heads. Every time you change the scope of the map, such as moving, zooming, zooming, etc., you need to request the interface data again, get the avatar data of the player within the viewable range of the current new map, and then draw the new avatar. Since Google Map needs to generate a Google Map OverlayView object when drawing a custom graph, its onAdd and onDraw methods are implemented. If we created an OverlayView object every time we drew a new avatar, it would increase the memory usage of the browser, and it would take some time to create an OverlayView object. In order to draw efficiently and spend as little memory as possible, a pool of OverlayView objects with a capacity of 200 can be created in advance. After each operation of changing the map scope, OverlayView objects that are not in the visible range can be recycled and put into the pool. Then, when drawing a new avatar, you can directly take an OverlayView object from the pool and use it. In this way, the memory usage is reduced, and the time spent on each new OverlayView object is also reduced. When no OverlayView object is available in the pool, the current page has reached the upper limit of 200 avatars and no more avatars need to be drawn.
/ / pseudo code
// Initialize pools
const MAX_COUNT = 200
function init() {
// Initial marker instance pool
this.pools = new MarkerPool(MAX_COUNT)
// Listen for idle events
google.maps.event.addListener(this.map, "idle".(a)= > {
this.isIdle = true
// Reclaim the marker outside the viewable area
this.reclaimMarker()
// Refresh data periodically
this.initRefreshTimer()
// Request user data
this.fetchUserData("nearbyMarker", { users: [] }, true)})}Copy the code
Idle execution
In order to ensure the best smoothness when manipulating maps, we don’t do anything like drag, zoom, zoom, etc., we don’t draw avatars, we don’t ask for data, we just let Google Maps change the map itself. When the Status of Google Map is idle, we will draw the profile picture or update the data. Google Map provides idle events that we only need to listen for.
Update data, that is, the interface request to the data, first do some cleaning, and then the qualified data update to be drawn in the avatar queue; It can be reduced to the lowest priority, only when the current drawing avatar queue is empty, to perform the data update task. Its execution time is basically fixed and predictable, not particularly delayed until the current frame is drawn. You can put it in the requestIdleCallback queue, and by adding a timeout period, it is executed only when the browser is idle or has exceeded a certain time.
/ / pseudo code
// Put the update data operation into the requestIdleCallback
function fetchUserData(type, userData, fromStart = true) {
// Add to the array to be drawn
if (data.users.length) {
this.patchUserData(data.users, type)}const nextType
// ... //
this.fetchUserDataApi(
this.myLocation,
this.mapBounds,
fromStart,
nextType
).then(data= > {
this.engine.pushRequest((a)= > {
this.fetchUserData(nextType, data)
})
})
}
Copy the code
To optimize the
The browser’s ideal frame rate is 60fps, and if it stays around 60fps, it’s pretty smooth. For a phone like Android 6, it’s definitely not going to reach 60fps, so you just have to increase the frame rate as much as possible to keep it stable around 30fps, and that’s pretty much it. For some details of the optimization, especially to avoid layout reflow situation, also seriously affect the page fluency. The following performance analysis was done by dragging the map page by lowering the CPU by 6 times and drawing 200 heads.
Avoid setting zIndex
Initially, each OverlayView is set to zIndex = ’50’, and the current user’s OverlayView is set to zIndex = ’80’, so that the current user is always displayed at the top. Changing the avatar style in this way will make the design look as good as it looks, but it will cause the page to be very sluggish, which we will see through chrome Performance debugging.
The average frame rate of a page is 8fps, which means it takes around 122ms to draw a frame. Looking at the Composite Layers step, regardless of other factors affecting the frame rate, it took 17.56ms. At an ideal 60fps, it takes around 16.67ms to draw a frame. Obviously, our Composite Layers step severely impacted performance. Composite Layers is the last step in the browser’s drawing of a frame, the Composite layer. Each time a new zIndex value is set, a new layer will be created. The elements with the same zIndex value will be drawn in the same layer. Let’s get rid of zIndex and see what happens.
With zIndex removed, the average frame rate on a page is now 10fps and the average time it takes to draw a frame is 100ms. The time spent in the Composite Layers stage was about 9ms. It’s obviously improved.
Avoid triggering reflow by accessing offsetWidth in onDraw
Since Google Maps keeps calling our onDraw method while dragging and dropping maps. In the onDraw method, you can optionally set the style and position of the current OverlayView object. Before optimization, the left and top of the current OverlayView object are calculated according to the coordinates converted from the width and height of the current container div and the current latitude and longitude.
// The code is as follows
/* Inherit from Google.maps. OverlayView to achieve draw */
function draw() {
const overlayProjection = this.overlayView.getProjection()
const posPixel = overlayProjection.fromLatLngToDivPixel(this.latLng)
const scale = this.computeScale()
// Resize the image's div to fit the indicated dimensions.
const div = this.el
let x = posPixel.x - div.offsetWidth / 2
let y = posPixel.y - (div.offsetHeight * (scale + 1)) / 2
div.style.transform = `scale(${scale}) `
div.style.left = x + "px"
div.style.top = y + "px"
}
Copy the code
In the draw method, call div.offsetwidth and div.offsetheight to force reflow, which can have a significant impact on performance. We can optimize it so that the head display is fixed in width and height. For example, let x = pospixel.x-32/2; And let y = posPixel. Y – 32 * (scale + 1) / 2; .
As you can see, Layout and Recalculate Style are now removed from the draw method. Now the average frame rate is roughly 12fps, and the average time it takes to draw a frame is 84ms. It’s improved again.
Necessary downgrading to remove unnecessary elements
On devices like Android 6, there’s no need to draw a bottom triangle for each avatar. The triangle at the bottom creates an extra div element, and if it’s 200 heads, it creates 200 more elements on the page. And frequent calls to onDraw during drag-and-drop operations will redraw each head, as well as each bottom triangle, which will also increase the time required to draw. For lower-end devices, such as Android 6 and below, you can remove the bottom triangle.
The average page frame rate is now 15fps, and the average time it takes to draw a frame is 68ms.
After tweaking these small details, when the CPU was reduced to 6 times slower and 200 heads were drawn on the page, the frame rate increased by 15fps from 8fps, fully doubling performance. For extreme devices like Android 6, you can actually scale it down from 200 to 100.
After dropping down to 100 heads, you can see that the average frame rate on a page is now 27fps, and the average time it takes to draw a frame is 36ms. The frame rate has been increased by 27fps from the previous 8fps, more than tripling performance.
summary
Front-end can also use some basic data structures and algorithms, combined with some front-end knowledge, can have a good practice. Before starting coding, you can first think about the general idea of implementation, whether there can be a better solution. When the low-end models cannot meet the performance requirements, learn to communicate with PM to see if they can be downgraded. Learn how to use tools to analyze and locate performance bottlenecks.