development

큰 선택 목록으로 인해 jQuery UI 자동 완성 콤보 상자가 매우 느림

big-blog 2020. 11. 27. 21:00
반응형

큰 선택 목록으로 인해 jQuery UI 자동 완성 콤보 상자가 매우 느림


http://jqueryui.com/demos/autocomplete/#combobox 와 같이 jQuery UI Autocomplete Combobox의 수정 된 버전을 사용하고 있습니다.

이 질문을 위해 정확히 그 코드가 있다고합시다 ^^^

버튼을 클릭하거나 콤보 상자 텍스트 입력에 초점을 맞춰 콤보 상자를 열 때 항목 목록을 표시하기 전에 큰 지연이 있습니다. 이 지연은 선택 목록에 더 많은 옵션이있을 때 눈에 띄게 커집니다.

이 지연은 처음에만 발생하는 것이 아니라 매번 발생합니다.

이 프로젝트의 선택 목록 중 일부는 매우 크기 때문에 (수백 수백 개의 항목) 지연 / 브라우저 정지는 용납 할 수 없습니다.

누구든지 이것을 최적화하기 위해 올바른 방향으로 나를 가리킬 수 있습니까? 아니면 성능 문제가 어디에있을 수 있습니까?

문제가 스크립트가 항목의 전체 목록을 표시하는 방식과 관련이 있다고 생각합니다 (빈 문자열에 대한 자동 완성 검색을 수행함). 모든 항목을 표시하는 다른 방법이 있습니까? 아마도 모든 정규식 일치를 수행하지 않는 모든 항목을 표시하는 일회성 케이스를 만들 수 있습니까 (입력을 시작하기 전에 목록을 여는 것이 일반적이므로)?

여기에 jsfiddle이 있습니다 : http://jsfiddle.net/9TaMu/


현재 콤보 박스 구현에서는 드롭 다운을 확장 할 때마다 전체 목록이 비워지고 다시 렌더링됩니다. 또한 전체 목록을 얻으려면 빈 검색을 수행해야하므로 minLength를 0으로 설정해야합니다.

다음은 자동 완성 위젯을 확장하는 자체 구현입니다. 내 테스트에서는 IE 7과 8에서도 5000 개 항목의 목록을 매우 원활하게 처리 할 수 ​​있습니다. 전체 목록을 한 번만 렌더링하고 드롭 다운 버튼을 클릭 할 때마다 다시 사용합니다. 이것은 또한 minLength = 0 옵션의 종속성을 제거합니다. 또한 배열 및 목록 소스로서 ajax와 함께 작동합니다. 또한 큰 목록이 여러 개있는 경우 위젯 초기화가 대기열에 추가되어 브라우저를 고정하지 않고 백그라운드에서 실행할 수 있습니다.

<script>
(function($){
    $.widget( "ui.combobox", $.ui.autocomplete, 
        {
        options: { 
            /* override default values here */
            minLength: 2,
            /* the argument to pass to ajax to get the complete list */
            ajaxGetAll: {get: "all"}
        },

        _create: function(){
            if (this.element.is("SELECT")){
                this._selectInit();
                return;
            }

            $.ui.autocomplete.prototype._create.call(this);
            var input = this.element;
            input.addClass( "ui-widget ui-widget-content ui-corner-left" );

            this.button = $( "<button type='button'>&nbsp;</button>" )
            .attr( "tabIndex", -1 )
            .attr( "title", "Show All Items" )
            .insertAfter( input )
            .button({
                icons: { primary: "ui-icon-triangle-1-s" },
                text: false
            })
            .removeClass( "ui-corner-all" )
            .addClass( "ui-corner-right ui-button-icon" )
            .click(function(event) {
                // close if already visible
                if ( input.combobox( "widget" ).is( ":visible" ) ) {
                    input.combobox( "close" );
                    return;
                }
                // when user clicks the show all button, we display the cached full menu
                var data = input.data("combobox");
                clearTimeout( data.closing );
                if (!input.isFullMenu){
                    data._swapMenu();
                    input.isFullMenu = true;
                }
                /* input/select that are initially hidden (display=none, i.e. second level menus), 
                   will not have position cordinates until they are visible. */
                input.combobox( "widget" ).css( "display", "block" )
                .position($.extend({ of: input },
                    data.options.position
                    ));
                input.focus();
                data._trigger( "open" );
            });

            /* to better handle large lists, put in a queue and process sequentially */
            $(document).queue(function(){
                var data = input.data("combobox");
                if ($.isArray(data.options.source)){ 
                    $.ui.combobox.prototype._renderFullMenu.call(data, data.options.source);
                }else if (typeof data.options.source === "string") {
                    $.getJSON(data.options.source, data.options.ajaxGetAll , function(source){
                        $.ui.combobox.prototype._renderFullMenu.call(data, source);
                    });
                }else {
                    $.ui.combobox.prototype._renderFullMenu.call(data, data.source());
                }
            });
        },

        /* initialize the full list of items, this menu will be reused whenever the user clicks the show all button */
        _renderFullMenu: function(source){
            var self = this,
                input = this.element,
                ul = input.data( "combobox" ).menu.element,
                lis = [];
            source = this._normalize(source); 
            input.data( "combobox" ).menuAll = input.data( "combobox" ).menu.element.clone(true).appendTo("body");
            for(var i=0; i<source.length; i++){
                lis[i] = "<li class=\"ui-menu-item\" role=\"menuitem\"><a class=\"ui-corner-all\" tabindex=\"-1\">"+source[i].label+"</a></li>";
            }
            ul.append(lis.join(""));
            this._resizeMenu();
            // setup the rest of the data, and event stuff
            setTimeout(function(){
                self._setupMenuItem.call(self, ul.children("li"), source );
            }, 0);
            input.isFullMenu = true;
        },

        /* incrementally setup the menu items, so the browser can remains responsive when processing thousands of items */
        _setupMenuItem: function( items, source ){
            var self = this,
                itemsChunk = items.splice(0, 500),
                sourceChunk = source.splice(0, 500);
            for(var i=0; i<itemsChunk.length; i++){
                $(itemsChunk[i])
                .data( "item.autocomplete", sourceChunk[i])
                .mouseenter(function( event ) {
                    self.menu.activate( event, $(this));
                })
                .mouseleave(function() {
                    self.menu.deactivate();
                });
            }
            if (items.length > 0){
                setTimeout(function(){
                    self._setupMenuItem.call(self, items, source );
                }, 0);
            }else { // renderFullMenu for the next combobox.
                $(document).dequeue();
            }
        },

        /* overwrite. make the matching string bold */
        _renderItem: function( ul, item ) {
            var label = item.label.replace( new RegExp(
                "(?![^&;]+;)(?!<[^<>]*)(" + $.ui.autocomplete.escapeRegex(this.term) + 
                ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>" );
            return $( "<li></li>" )
                .data( "item.autocomplete", item )
                .append( "<a>" + label + "</a>" )
                .appendTo( ul );
        },

        /* overwrite. to cleanup additional stuff that was added */
        destroy: function() {
            if (this.element.is("SELECT")){
                this.input.remove();
                this.element.removeData().show();
                return;
            }
            // super()
            $.ui.autocomplete.prototype.destroy.call(this);
            // clean up new stuff
            this.element.removeClass( "ui-widget ui-widget-content ui-corner-left" );
            this.button.remove();
        },

        /* overwrite. to swap out and preserve the full menu */ 
        search: function( value, event){
            var input = this.element;
            if (input.isFullMenu){
                this._swapMenu();
                input.isFullMenu = false;
            }
            // super()
            $.ui.autocomplete.prototype.search.call(this, value, event);
        },

        _change: function( event ){
            abc = this;
            if ( !this.selectedItem ) {
                var matcher = new RegExp( "^" + $.ui.autocomplete.escapeRegex( this.element.val() ) + "$", "i" ),
                    match = $.grep( this.options.source, function(value) {
                        return matcher.test( value.label );
                    });
                if (match.length){
                    match[0].option.selected = true;
                }else {
                    // remove invalid value, as it didn't match anything
                    this.element.val( "" );
                    if (this.options.selectElement) {
                        this.options.selectElement.val( "" );
                    }
                }
            }                
            // super()
            $.ui.autocomplete.prototype._change.call(this, event);
        },

        _swapMenu: function(){
            var input = this.element, 
                data = input.data("combobox"),
                tmp = data.menuAll;
            data.menuAll = data.menu.element.hide();
            data.menu.element = tmp;
        },

        /* build the source array from the options of the select element */
        _selectInit: function(){
            var select = this.element.hide(),
            selected = select.children( ":selected" ),
            value = selected.val() ? selected.text() : "";
            this.options.source = select.children( "option[value!='']" ).map(function() {
                return { label: $.trim(this.text), option: this };
            }).toArray();
            var userSelectCallback = this.options.select;
            var userSelectedCallback = this.options.selected;
            this.options.select = function(event, ui){
                ui.item.option.selected = true;
                if (userSelectCallback) userSelectCallback(event, ui);
                // compatibility with jQuery UI's combobox.
                if (userSelectedCallback) userSelectedCallback(event, ui);
            };
            this.options.selectElement = select;
            this.input = $( "<input>" ).insertAfter( select )
                .val( value ).combobox(this.options);
        }
    }
);
})(jQuery);
</script>

map () 함수가 느리게 보였기 때문에 결과가 반환되는 방식 ( 소스 함수에서)을 수정했습니다 . 큰 선택 목록 (및 더 작은 목록)의 경우 더 빠르게 실행되지만 수천 개의 옵션이있는 목록은 여전히 ​​매우 느립니다. (방화범의 프로필 기능으로) 원본 코드와 수정 된 코드를 프로파일 링했으며 실행 시간은 다음과 같습니다.

원본 : 프로파일 링 (372.578ms, 42307 호출)

수정 됨 : 프로파일 링 (0.082ms, 3 회 호출)

다음은 소스 함수 의 수정 된 코드입니다 . jquery ui 데모 http://jqueryui.com/demos/autocomplete/#combobox 에서 원본 코드를 볼 수 있습니다 . 확실히 더 많은 최적화가있을 수 있습니다.

source: function( request, response ) {
    var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), "i" );
    var select_el = this.element.get(0); // get dom element
    var rep = new Array(); // response array
    // simple loop for the options
    for (var i = 0; i < select_el.length; i++) {
        var text = select_el.options[i].text;
        if ( select_el.options[i].value && ( !request.term || matcher.test(text) ) )
            // add element to result array
            rep.push({
                label: text, // no more bold
                value: text,
                option: select_el.options[i]
            });
    }
    // send response
    response( rep );
},

도움이 되었기를 바랍니다.


나는 Berro의 대답을 좋아합니다. 그러나 여전히 약간 느리기 때문에 (선택에 약 3000 개의 옵션이 있음) 처음 N 개의 일치하는 결과 만 표시되도록 약간 수정했습니다. 또한 마지막에 사용자에게 더 많은 결과를 사용할 수 있음을 알리고 포커스를 취소하고 해당 항목에 대한 이벤트를 선택하는 항목을 추가했습니다.

다음은 소스 및 선택 기능에 대한 수정 된 코드이며 초점을 위해 추가 된 코드입니다.

source: function( request, response ) {
    var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), "i" );
    var select_el = select.get(0); // get dom element
    var rep = new Array(); // response array
    var maxRepSize = 10; // maximum response size  
    // simple loop for the options
    for (var i = 0; i < select_el.length; i++) {
        var text = select_el.options[i].text;
        if ( select_el.options[i].value && ( !request.term || matcher.test(text) ) )
            // add element to result array
            rep.push({
                label: text, // no more bold
                value: text,
                option: select_el.options[i]
            });
        if ( rep.length > maxRepSize ) {
            rep.push({
                label: "... more available",
                value: "maxRepSizeReached",
                option: ""
            });
            break;
        }
     }
     // send response
     response( rep );
},          
select: function( event, ui ) {
    if ( ui.item.value == "maxRepSizeReached") {
        return false;
    } else {
        ui.item.option.selected = true;
        self._trigger( "selected", event, {
            item: ui.item.option
        });
    }
},
focus: function( event, ui ) {
    if ( ui.item.value == "maxRepSizeReached") {
        return false;
    }
},

우리는 똑같은 것을 찾았지만 결국 우리의 해결책은 더 작은 목록을 갖는 것이 었습니다!

내가 그것을 살펴 보았을 때 그것은 여러 가지의 조합이었습니다.

1) The contents of the list box is cleared and re-built every time the list box is shown (or the user types something in and starts to filter the list). I think that this is mostly unavoidable and fairly core to the way the list box works (as you need to remove items from the list in order for filtering to work).

You could try changing it so that it shows and hides items in the list rather than completely re-constructing it again, but it would depend on how your list is constructed.

The alternative is to try and optimise the clearing / construction of the list (see 2. and 3.).

2) There is a substantial delay when clearing the list. My theory is that this is at least party due to every list item having data attached (by the data() jQuery function) - I seem to remember that removing the data attached to each element substantially sped up this step.

You might want to look into more efficient ways of removing child html elements, for example How To Make jQuery.empty Over 10x Faster. Be careful of potentially introducing memory leaks if you play with alternative empty functions.

Alternatively you might want to try to tweak it so that data isn't attached to each element.

3) The rest of the delay is due to the construction of the list - more specifically the list is constructed using a large chain of jQuery statements, for example:

$("#elm").append(
    $("option").class("sel-option").html(value)
);

This looks pretty, but is a fairly inefficient way of constructing html - a much quicker way is to construct the html string yourself, for example:

$("#elm").html("<option class='sel-option'>" + value + "</option>");

See String Performance: an Analysis for a fairly in-depth article on the most efficient way of concatenating strings (which is essentially what is going on here).


Thats where the problem is, but I honestly don't know what the best way of fixing it would be - in the end we shortened our list of items so it wasn't a problem any more.

By addressing 2) and 3) you may well find that the performance of the list improves to an acceptable level, but if not then you will need to address 1) and try to come up with an alternative to clearing and re-building the list every time it is displayed.

Surprisingly the function filtering the list (which involved some fairly complex regular expressions) had very little effect on the performance of the drop down - you should check to make sure that you have not done something silly, but for us this wasn't the performance bottlekneck.


What I have done I am sharing:

In the _renderMenu, I've written this:

var isFullMenuAvl = false;
    _renderMenu: function (ul, items) {
                        if (requestedTerm == "**" && !isFullMenuAvl) {
                            var that = this;
                            $.each(items, function (index, item) {
                                that._renderItemData(ul, item);
                            });
                            fullMenu = $(ul).clone(true, true);
                            isFullMenuAvl = true;
                        }
                        else if (requestedTerm == "**") {
                            $(ul).append($(fullMenu[0].childNodes).clone(true, true));
                        }
                        else {
                            var that = this;
                            $.each(items, function (index, item) {
                                that._renderItemData(ul, item);
                            });
                        }
                    }

This is mainly for server side request serving. But it can used for local data. We are storing requestedTerm and checking if it matches with ** which means full menu search is going on. You can replace "**" with "" if you are searching full menu with "no search string". Please reach me for any type of queries. It improves performance in my case for at least 50%.

참고URL : https://stackoverflow.com/questions/5073612/jquery-ui-autocomplete-combobox-very-slow-with-large-select-lists

반응형