How to Improve Your Website Performance | The Performance Testing Guide

The Performance Guidepost

How do you improve your website performance? What actions do you take when it’s not going north? More importantly, how do you make sense of a spike in website traffic and sustain it? What tools help you carry out website performance testing? Let’s explore all these areas together in this blog post.

Imagine you open your analytics dashboard like every other day. But today is different! Today, you notice the number of visitors has touched a new high. Sure brings a smile on your face. You continue tracking it and it keeps increasing. However, after a few hours it suddenly drops all of a sudden.

You wonder what happened and try to understand the events that unfolded. Why the drop in visits? You discover that your colleague had started a cool promotion, which brought in an unusual amount of traffic. This increased the website load multifold and the site could not take the load and crashed.

A poor show, in spite of the excellent job by your marketing team.

What could have been done to avoid such a situation. How could you have offered a better customer experience through excellent website usability? And avoided the drop in site visitors, and perhaps sales?

Website performance testing is the answer to unanticipated site load.

What is Website Performance Testing?

Triple “S” check is a must for public facing websites looking to improve website performance.

  • Speed – Determines whether the application responds quickly.
  • Scalability – Determines maximum user load the software application can handle.
  • Stability – Determines if the application is stable under varying loads.

And all three can be measured using performance testing.

Performance testing is the investigation done either to determine or to prove the response time, scalability and performance of a website. This ensures the website performs well under the expected regular workload, at peak load and uncover inconsistencies across different operating systems or devices.

The goal of performance testing is not to find bugs but to eliminate performance bottlenecks and tune the system for maximum load. This is also to determine the maximum threshold the website can take.

Common Performance Problems
Poor Response Time:

Once the user performs an action and she has to wait for so long before the response is provided. This can lead to poor user experience and high exit rate.

Poor Load Distribution:

Poor load distribution can cause slow response time by incorrectly assigning new site visitors to hanged up servers. If too many people are on the same server, they’re going to experience difficulties, even if the overall system is well under capacity. Check the sites of some of the big players and you will notice the site loads in a flash of moments.

Types of Performance Testing
Load Test:

Generally, a load test is conducted to understand the behavior of a system under the specific expected load. It helps identify the maximum operating capacity of an application as well as any bottlenecks and determine which element is causing degradation. For example, if the number of users increases then how much CPU, memory will be consumed, what is the network and bandwidth response time.

It also helps in measuring the response time, throughput rates, and resource-utilization levels, and to identify the breaking point, and the peak load the website can handle.

This can answer specific performance related questions. Like, what is the maximum number of users that can use the system without any impact on performance and acceptable response time?

Stress Test:

Stress testing refers to the testing of a website to determine whether its performance is satisfactory under any extreme and unfavourable conditions. These conditions may occur as a result of heavy network traffic, process loading and maximum requests for resource utilization. Stress testing enables to identify how the website behaves under extreme load conditions.

This will answer questions like what is the maximum peak load the system can handle and determine if the system will perform sufficiently if the current load goes well above the expected maximum limits?

Soak/Endurance Test:

This is usually done to determine if the system can sustain the continuous expected load. This helps in detecting potential memory leaks and utilization. Also to check the performance degradation when the system is being used for long duration.

This can answer questions like, if the promotion becomes very popular and the load of system is way beyond for a very long duration, can the system handle such situations?

Spike Test:

Spike testing is a subset of stress testing.  A spike test is a type of performance test focused on determining reaction to a sudden large spikes in the load generated by users.

This lets you know if the system can handle a sudden spike of users when a competitor promotion results in users visiting your site because you’re already running similar promotions.

Tools

Lots of performance testing tools are available for different types of tests and it is quite difficult to cover all types of test using one.

Jmeter is one of the renowned open source tool designed to load test functional behavior and measure performance.

Let’s explore more about Jmeter and its functioning in the upcoming blog.

Meet User Expectations with Proper Performance Testing

Performance Testing is a must before the website goes to market as poor performance and inconsistent behavior of the site may lead to inadequate reputation, poor user experience and will not meet the sales goals.

It’s a must to perform performance testing at the initial stages of building a website and regularly at different intervals. Analytics can help monitor the peak loads and help plan to improve website performance. This can go a long way in building customer trust, relation and not only retain but expand customer base. Let’s read about that too in another upcoming blog.

Create Custom Xtype For Pathfield BrowseDialog in AEM

Custom Xtype For Pathfield BrowseDialog

Sometimes the default xtypes provided by AEM doesn’t fulfill our requirements. So we need to design our custom xtypes for pathfield BrowseDialog on a regular basis.

Problem with the Default xtype in AEM:

While working with xtype pathfield, I stumbled upon a use case/problem. I wanted a pathfield so that I can choose the product under /etc/commerce/products/accunity/en_us/products hierarchy by a productName property.

Note: Generally, pathfield can show the nodes by its name or jcr:title.

path field

Steps I followed to Solve This xtype Error:
  1. I created a widget of xtype:pathfield
  2. If I opened my  dialog, I could select any values under /content
  3. So I needed to set rootPath in the widget
    rootPath:/etc/commerce/products/accunity/en_us/products
  4. The nodes under products are of type nt:unstructured. By default, pathfield doesn’t allow this types of nodes in the tree hierarchy.
  5. So added a property predicate: nosystem

Now the pathfield looked like this:

how the pathfield looked like after creating a widget of xtype:pathfield

But still, it is a very tedious task for the author to select a particular product. I wanted the pathfield to show the productName in place of node-name. So I decided to write a custom xtype.

But How did I Write the Custom xtype?

The first question here is how this tree structure shows up here:

Product PathField in AEM xtype

So while debugging my dialog, I found, it calls currentPath.ext.json and shows the “name” property of  JSON in the tree hierarchy.

So the next step for me was to change this servlet.

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.sling.SlingServlet;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.commons.json.JSONException;
import org.apache.sling.commons.json.JSONObject;

import java.io.IOException;
import java.util.Iterator;


@Component
@SlingServlet(generateComponent = false, resourceTypes = "sling/servlet/default", selectors = {"path"}, extensions = {"json"})

public class MyServlet extends SlingSafeMethodsServlet {
   protected void doGet(SlingHttpServletRequest request,SlingHttpServletResponse response)throws IOException
   {
       if (request.getRequestPathInfo().getSelectors()[0].equals("path")) {
           String resourcePath = request.getRequestPathInfo().getResourcePath();
           Resource resource = request.getResourceResolver().getResource(resourcePath);
           if (resource != null) {
               Iterator<Resource> iter = resource.listChildren();
               JSONArray jsonArray = new JSONArray();
               JSONObject jsonObject = null;
               while (iter.hasNext()) {
                   Resource childResource = iter.next();
                   if (!childResource.getName().equals("image")) {
                       ValueMap valueMap = childResource.adaptTo(ValueMap.class);
                       jsonObject = new JSONObject();
                       try {
                           jsonObject.put("name", childResource.getName());
                           if (valueMap.containsKey("productName"))
                               jsonObject.put("text", valueMap.get("productName", ""));
                           else if (valueMap.containsKey("jcr:title"))
                               jsonObject.put("text", valueMap.get("jcr:title", ""));
                           else
                               jsonObject.put("text", childResource.getName());
                           jsonObject.put("type", valueMap.get("jcr:primaryType", ""));
                           if (valueMap.get("jcr:primaryType").equals("sling:folder"))
                               jsonObject.put("cls", "folder");
                           else
                               jsonObject.put("cls", "file");
                           jsonArray.put(jsonObject);
                       } catch (JSONException e) {
                           e.printStackTrace();
                       }
                   }
               }
               response.getWriter().print(jsonArray);
           }
       }

   }
}

The next question was from where this servlet is getting called.

The answer is browserDialog widget. Inside pathfield widget, it is calling browserDialog to show this tree structure.

Note: Go to browseDialog.js

Change this part:

tree structure in browsedialog in AEM

 

Here is the updated browseDialog.js

CQ.CustomBrowseDialog = CQ.Ext.extend(CQ.Dialog, {

    /**
     * The browse dialog's tree panel.
     * @private
     * @type CQ.Ext.tree.TreePanel
     */
    treePanel: null,

    /**
     * The browse dialog's browse field.
     * @private
     * @type CQ.form.BrowseField
     */
    browseField: null,

    initComponent: function(){
       CQ.CustomBrowseDialog.superclass.initComponent.call(this);
    },

    /**
     * Selects the specified path in the tree.
     * @param {String} path The path to select
     */
    loadContent: function(path) {
        if (typeof path == "string") {
            this.path = path;
            this.treePanel.selectPath(path,"name");
            if(this.parBrowse){
                // reload paragraph store
                this.paraProxy.api["read"].url = CQ.HTTP.externalize(path, true) + ".paragraphs.json";
                this.paraStore.reload();
            }
        }
    },

    /**
     * Returns the path of the selected tree node (or an empty string if no
     * tree node has been selected yet).
     * @return {String} The path
     */
    getSelectedPath: function() {
        try {
            return this.treePanel.getSelectionModel().getSelectedNode().getPath();
        } catch (e) {
            return "";
        }
    },

    /**
     * Returns the anchor of the selected paragraph (or an empty string if
     * no paragraph has been selected yet).
     * @return {String} The anchor
     */
    getSelectedAnchor: function() {
        try {
            var anchorID = this.data.getSelectedRecords()[0].get("path");
            anchorID = anchorID.substring(anchorID.indexOf("jcr:content")
                    + "jcr:content".length + 1);
            return anchorID.replace(/\//g, "_").replace(/:/g, "_");
        } catch (e) {
            return "";
        }
    },

    constructor: function(config){

        var treeRootConfig = CQ.Util.applyDefaults(config.treeRoot, {
            "name": "content",
            "text": CQ.I18n.getMessage("Site"),
            "draggable": false,
            "singleClickExpand": true,
            "expanded":true
        });

        var treeLoaderConfig = CQ.Util.applyDefaults(config.treeLoader, {
            "dataUrl": CQ.HTTP.externalize("/content.path.json"),
            "requestMethod":"GET",
            "baseParams": {
                "predicate": "hierarchy",
                "_charset_": "utf-8"
            },
            "baseAttrs": {
                "singleClickExpand":true
            },
            "listeners": {
                "beforeload": function(loader, node){
                    this.dataUrl = node.getPath() + ".path.json";
                }
            }
        });

        this.treePanel = new CQ.Ext.tree.TreePanel({
            "region":"west",
            "lines": CQ.themes.BrowseDialog.TREE_LINES,
            "bodyBorder": CQ.themes.BrowseDialog.TREE_BORDER,
            "bodyStyle": CQ.themes.BrowseDialog.TREE_STYLE,
            "height": "100%",
            "width": 200,
            "autoScroll": true,
            "containerScroll": true,
            "root": new CQ.Ext.tree.AsyncTreeNode(treeRootConfig),
            "loader": new CQ.Ext.tree.TreeLoader(treeLoaderConfig),
            "defaults": {
                "draggable": false
            }
        });

        var width = CQ.themes.BrowseDialog.WIDTH;
        var items = this.treePanel;

        if (config.parBrowse) {
            this.treePanel.on("click", this.onSelectPage.createDelegate(this));

            // Paragraph store
            var reader = new CQ.Ext.data.JsonReader({
                "id":            "path",
                "root":          "paragraphs",
                "totalProperty": "count",
                "fields":        [ "path", "html" ]
            });
            this.paraProxy = new CQ.Ext.data.HttpProxy({
                "url": "/"
            });
            this.paraStore = new CQ.Ext.data.Store({
                "proxy":    this.paraProxy,
                "reader":   reader,
                "autoLoad": false
            });

            // Paragraph template
            var paraTemplate = new CQ.Ext.XTemplate(
                '<tpl for=".">',
                    '<div class="cq-paragraphreference-paragraph">{html}</div>',
                '</tpl>'
            );

            // Paragraph view
            this.data = new CQ.Ext.DataView({
                "id": "cq-paragraphreference-data",
                "region": "center",
                "store": this.paraStore,
                "tpl": paraTemplate,
                "itemSelector": "div.cq-paragraphreference-paragraph",
                "selectedClass": "cq-paragraphreference-selected",
                "singleSelect": true,
                "style": { "overflow": "auto" }
            });

            // init dialog width and fields
            width = 550;
            items = new CQ.Ext.Panel({
                "border":false,
                "layout": "border",
                "items": [ this.treePanel, this.data ]
            });
        }

        CQ.Util.applyDefaults(config, {
            "title": CQ.I18n.getMessage("Select Path"),
            "closable": true,
            "width": width,
            "height": CQ.themes.BrowseDialog.HEIGHT,
            "minWidth": CQ.themes.BrowseDialog.MIN_WIDTH,
            "minHeight": CQ.themes.BrowseDialog.MIN_HEIGHT,
            "resizable": CQ.themes.BrowseDialog.RESIZABLE,
            "resizeHandles": CQ.themes.BrowseDialog.RESIZE_HANDLES,
            "autoHeight": false,
            "autoWidth": false,
            "cls":"cq-browsedialog",
            "ok": function() { this.hide(); },
            "buttons": CQ.Dialog.OKCANCEL,
            "items": items
        });
       CQ.CustomBrowseDialog.superclass.constructor.call(this, config);
    },

    /**
     * @private
     */
    onSelectPage: function(node, event) {
        this.paraProxy.api["read"].url = CQ.HTTP.externalize(node.getPath() + ".paragraphs.json", true);
        this.paraStore.reload();
    }
});

CQ.Ext.reg('recipebrowsedialog',CQ.CustomBrowseDialog);

We can’t make this change in the /libs section. So, I made my own xtype as productPathfield and add a custom browseDialog in pathfield.js with this modification.

Note: xtype pathfield doesn’t fulfill my requirements so needed to change it with productPathfield.

CQ.form.CustomPathField = CQ.Ext.extend(CQ.Ext.form.ComboBox, {

    /**
     * Remembers the last value when the last key up happened.
     * @type String
     * @private
     */
    lastValue: null,

    /**
     * The ID of the delayed search interval.
     * @type Number
     * @private
     */
    searchIntervalId: 0,

    /**
     * The panel holding the link-browser.
     * @type CQ.BrowseDialog
     * @private
     */
    browseDialog: null,

    /**
     * Returns the anchor of the selected paragraph (or an empty string if
     * no paragraph has been selected yet).
     * @return {String} The anchor
     */
    getParagraphAnchor: function() {
        return this.browseDialog.getSelectedAnchor();
    },

    /**
     * Checks if the current path is quoted. If yes the new value is decorated
     * with quotes as well.
     * @private
     */
    adjustNewValue: function(currentValue, newValue) {
        if (/^path:"/.test(currentValue)) {
            // current value starts with quotes: decorate with quotes
            // (add final quotes even if they do not exist yet - otherwise
            // the triggered search would fail ('path:"/content')
            newValue = '"' + newValue + '"';
        }
        return newValue;
    },

    /**
     * Executed on key up in the control.
     * - Checks if its value matches a path. If yes, request .pages.json
     * @private
     */
    keyup: function(comp, evt) {
        var currentValue = this.getRawValue();

        var key = evt.getKey();
        if (key == 13) {
            // [enter] hit
            this.fireEvent("search", this, currentValue);
        }

        if (currentValue == this.lastValue) {
            // value did not change (key was arrows, ctrl etc.)
            return;
        }
        this.lastValue = currentValue;

        var path = currentValue;

        if (/^\//.test(path) && /\/$/.test(path)) {
            // path starts with a slash: ignore non-absolute path (#29745)
            // path ends with a slash: request path.pages.json
            if (path == "/") {
                path = this.rootPath ? this.rootPath : "/";
            }
            else {
                // remove final slash:
                path = path.replace(/\/$/, "");
            }
            this.loadStore(CQ.shared.HTTP.encodePath(path));
        }
        else if (this.searchDelay) {
            window.clearTimeout(this.searchIntervalId);
            var pc = this;
            this.searchIntervalId = window.setTimeout(function() {
                pc.fireEvent("search", pc, currentValue);
            }, this.searchDelay);
        }

    },

    /**
     * Reloads the autocompletion store with a new URL.
     * @private
     */
    loadStore: function(path) {
        this.store.proxy.api["read"].url = path + ".pages.json";
        this.store.reload();
    },

    /**
     * The trigger action of the TriggerField, creates a new BrowseDialog
     * if it has not been created before, and shows it.
     * @private
     */
    onTriggerClick : function() {
        if (this.disabled) {
            return;
        }
        // lazy creation of browse dialog
        if (this.browseDialog == null || this.modeless) {
            function okHandler() {
                var path = this.getSelectedPath();
                var anchor = this.parBrowse ? this.getSelectedAnchor() : null;

                var value;
                if (anchor) {
                    value = CQ.Util.patchText(this.pathField.parLinkPattern, [path, anchor]);
                } else {
                    value = CQ.Util.patchText(this.pathField.linkPattern, path);
                }
                if (this.pathField.suffix) {
                    value += this.pathField.suffix;
                }

                this.pathField.setValue(value);

                this.pathField.fireEvent("dialogselect", this.pathField, path, anchor);
                this.hide();
            }

            var browseDialogConfig = CQ.Util.applyDefaults(this.browseDialogCfg, {
                ok: okHandler,
                // pass this to the BrowseDialog to make in configurable from 'outside'
                parBrowse: this.parBrowse,
                treeRoot: this.treeRoot,
                treeLoader: this.treeLoader,
                listeners: {
                    hide: function() {
                        if (this.pathField) {
                            this.pathField.fireEvent("dialogclose");
                        }
                    }
                },
                loadAndShowPath: function(path) {
                    this.path = path;
                    // if the root node is the real root, we need an additional slash
                    // at the begining for selectPath() to work properly
                    if (this.pathField.rootPath == "" || this.pathField.rootPath == "/") {
                        path = "/" + path;
                    }

                    var browseDialog = this;
                    var treePanel = this.treePanel;

                    // what to do when selectPath worked
                    function successHandler(node) {
                        // ensureVisible fails on root, ie. getParentNode() == null
                        if (node.parentNode) {
                            node.ensureVisible();
                        }
                        if (browseDialog.parBrowse) {
                            browseDialog.onSelectPage(node);
                        }
                    }

                    // string split helper function
                    function substringBeforeLast(str, delim) {
                        var pos = str.lastIndexOf(delim);
                        if (pos >= 0) {
                            return str.substring(0, pos);
                        } else {
                            return str;
                        }
                    }

                    // try to handle links created by linkPattern/parLinkPattern,
                    // such as "/content/foo/bar.html#par_sys"; needs to try various
                    // cut-offs until selectPath works (eg. /content/foo/bar)
                    // 1) try full link (path)
                    treePanel.selectPath(path, null, function(success, node) {
                        if (success && node) {
                            successHandler(node);
                        } else {
                            // 2) try and split typical anchor from (par)linkPattern
                            path = substringBeforeLast(path, "#");

                            treePanel.selectPath(path, null, function(success, node) {
                                if (success && node) {
                                    successHandler(node);
                                } else {
                                    // 3) try and split typical extension from (par)linkPattern
                                    path = substringBeforeLast(path, ".");

                                    treePanel.selectPath(path, null, function(success, node) {
                                        if (success && node) {
                                            successHandler(node);
                                        }
                                    });
                                }
                            });
                        }
                    });
                },
                pathField: this
            });

            // fix dialog width for par browse to include 3 cols of pars
            if (this.parBrowse) {
                browseDialogConfig.width = 570;
            }

            // build the dialog and load its contents
            this.browseDialog = new CQ.CustomBrowseDialog(browseDialogConfig);
        }

        this.browseDialog.loadAndShowPath(this.getValue());

        this.browseDialog.show();
        this.fireEvent("dialogopen");
    },

    constructor : function(config){
        // set default values
        // done here, because it is already used in below applyDefaults
        if (typeof config.rootTitle === "undefined") {
            config.rootTitle = config.rootPath || CQ.I18n.getMessage("Websites");
        }
        if (typeof config.rootPath === "undefined") {
            config.rootPath = "/content";
        }
        var rootName = config.rootPath;
        // the root path must not include a leading slash for the root tree node
        // (it's added automatically in CQ.Ext.data.Node.getPath())
        if (rootName.charAt(0) === "/") {
            rootName = rootName.substring(1);
        }
        if (typeof config.predicate === "undefined") {
            config.predicate = "siteadmin";
        }
        if (typeof config.showTitlesInTree === "undefined") {
            config.showTitlesInTree = true;
        }

        var pathField = "path";
        if (config.escapeAmp) {
            pathField = "escapedPath";
            delete config.escapeAmp;
        }

        CQ.Util.applyDefaults(config, {
            linkPattern: config.parBrowse ? "{0}.html" : "{0}",
            parLinkPattern: "{0}.html#{1}",

            tpl: new CQ.Ext.XTemplate(
                '<tpl for=".">',
                    '<div ext:qtip="{tooltip}" class="x-combo-list-item">',
                        '<span class="cq-pathfield-completion-list-name">{label}</span>',
                        '<span class="cq-pathfield-completion-list-title">{title}</span>',
                    '</div>',
                '</tpl>'),
            displayField: pathField,
            typeAhead: true,
            searchDelay: 200,
            suffix:"",
            mode: 'local',
            selectOnFocus:true,
            enableKeyEvents: true,
            validationEvent: false,
            validateOnBlur: false,
            // show a search icon
            triggerClass: "x-form-search-trigger",
            treeRoot: {
                name: rootName,
                // label for the root
                text: config.rootTitle
            },
            treeLoader: {
                dataUrl: CQ.shared.HTTP.getXhrHookedURL(CQ.Util.externalize(config.rootPath + ".ext.json")),
                baseParams: {
                    predicate: config.predicate,
                    "_charset_": "utf-8"
                },
                // overwriting method to be able to intercept node labeling
                createNode: function(attr) {
                    if (!config.showTitlesInTree) {
                        // no labled resources, use plain node name for tree nodes
                        attr.text = attr.name;
                    }
                    return CQ.Ext.tree.TreeLoader.prototype.createNode.call(this, attr);
                },
                // overwriting method to fix handling of array params
                // (needed for config.predicate string array case)
                getParams: function(node) {
                    var params = this.baseParams;
                    params.node = node.id;
                    return CQ.Ext.urlEncode(params);
                },
                listeners: {
                    beforeLoad: function(loader, node) {
                        this.dataUrl = node.getPath() + ".ext.json";
                    }
                }
            }
        });

        // store for autocompletion while typing
        if (!(config.store instanceof CQ.Ext.data.Store)) {
            var storeConfig = CQ.Util.applyDefaults(config.store, {
                // URL for proxy is set dynamically based on current path in loadStore()
                proxy: new CQ.Ext.data.HttpProxy({
                    url: "/",
                    method:"GET"
                }),
                baseParams: {
                    predicate: config.predicate
                },
                "reader": new CQ.Ext.data.JsonReader(
                    {
                        "totalProperty": "results",
                        "root": "pages",
                        "id": "path"
                    },
                    CQ.Ext.data.Record.create([
                        {
                            "name": "label",
                            "convert": function(v, rec) {return CQ.shared.XSS.getXSSValue(rec.label);}
                        },
                        {
                            "name": "title",
                            "mapping": CQ.shared.XSS.getXSSPropertyName("title")
                        },
                        {
                            "name": pathField
                        },
                        {
                            "name": "tooltip",
                            // have to encode this twice because the template decodes the value before 
                            // injecting it into the tooltip div
                            "convert": function(v, rec) {return _g.Util.htmlEncode(_g.Util.htmlEncode(rec.path));}
                        }
                    ])
                )
            });
            config.store = new CQ.Ext.data.Store(storeConfig);
        }
        this.store = config.store;

        CQ.form.CustomPathField.superclass.constructor.call(this, config);
    },

    initComponent : function(){
        CQ.form.CustomPathField.superclass.initComponent.call(this);

        this.addListener("keyup", this.keyup, this);

        this.addEvents(
            /**
             * @event search
             * Fires when the enter key is hit or after the user stopped typing.
             * The period between the last key press and the firing of the event
             * is specified in {@link #searchDelay}.
             * @param {CQ.form.CustomPathField} this
             * @param {String} value The current value of the field
             */
            'search',
            /**
             * @event dialogopen
             * Fires when the browse dialog is opened.
             * @param {CQ.form.CustomPathField} this
             */
            "dialogopen",
            /**
             * @event dialogselect
             * Fires when a new value is selected in the browse dialog.
             * @param {CQ.form.CustomPathField} this
             * @param {String} path The path selected in the tree of the browse dialog
             * @param {String} anchor The paragraph selected in the browse dialog (or null)
             */
            "dialogselect",
            /**
             * @event dialogclose
             * Fires when the browse dialog is closed.
             * @param {CQ.form.CustomPathField} this
             */
            "dialogclose"
        );
        
        // register component as drop target
        CQ.WCM.registerDropTargetComponent(this);
    },
    
    getDropTargets : function() {
        var pathFieldComponent = this;
        var target = new CQ.wcm.EditBase.DropTarget(this.el, {
            "ddAccept": "*/*",
            "notifyDrop": function(dragObject, evt, data) {
                if (dragObject && dragObject.clearAnimations) {
                    dragObject.clearAnimations(this);
                }
                if (data && data.records && data.records[0]) {
                    var pathInfo = data.records[0].get("path");
                    if (pathInfo) {
                        pathFieldComponent.setValue(pathInfo);
                        return true;
                    }
                }
                return false;
            }
        });
        target.groups["media"] = true;
        target.groups["s7media"] = true;
        target.groups["page"] = true;
        return [target];
    }
});

CQ.Ext.reg("productPathfield", CQ.form.CustomPathField);

After all the changes, we can see the desired results as follows.

product pathfield in AEM

Please leave your precious comments on what you think about this approach. Happy to learn better solution for the same problem!